- @if (!string.IsNullOrWhiteSpace(SettingsModel.HeroImageBase64))
+ @if (!string.IsNullOrWhiteSpace(SettingsModel.HeroImagePath) || !string.IsNullOrWhiteSpace(SettingsModel.HeroImageBase64))
{
-

+
diff --git a/src/BirthList.Web/Components/Pages/RegistryAdmin.razor.cs b/src/BirthList.Web/Components/Pages/RegistryAdmin.razor.cs
index e523c8e..9089269 100644
--- a/src/BirthList.Web/Components/Pages/RegistryAdmin.razor.cs
+++ b/src/BirthList.Web/Components/Pages/RegistryAdmin.razor.cs
@@ -5,6 +5,7 @@ using BirthList.Web.Services;
using Blazored.TextEditor;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
@@ -23,6 +24,7 @@ public partial class RegistryAdmin : ComponentBase
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
[Inject] private SmtpEmailSender EmailSender { get; set; } = null!;
[Inject] private SmtpConfigurationStatusService SmtpConfigurationStatusService { get; set; } = null!;
+ [Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!;
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
protected RegistrySettingsEditModel SettingsModel { get; } = new();
@@ -817,12 +819,16 @@ public partial class RegistryAdmin : ComponentBase
try
{
- const long maxFileSize = 10 * 1024 * 1024; // 10 MB
+ const long maxOriginalFileSize = 10 * 1024 * 1024; // 10 MB
+ const long maxProcessedFileSize = 3 * 1024 * 1024; // 3 MB
+ const int maxHeroImageWidth = 1920;
+ const int maxHeroImageHeight = 1080;
+
var file = e.File;
- if (file.Size > maxFileSize)
+ if (file.Size > maxOriginalFileSize)
{
- HeroImageUploadMessage = $"File size exceeds maximum of 10 MB";
+ HeroImageUploadMessage = "File size exceeds maximum of 10 MB";
return;
}
@@ -832,11 +838,46 @@ public partial class RegistryAdmin : ComponentBase
return;
}
- using var memoryStream = new MemoryStream();
- await file.OpenReadStream(maxFileSize).CopyToAsync(memoryStream).ConfigureAwait(false);
+ var targetContentType = file.ContentType.Equals("image/png", StringComparison.OrdinalIgnoreCase)
+ ? "image/png"
+ : "image/jpeg";
- SettingsModel.HeroImageBase64 = Convert.ToBase64String(memoryStream.ToArray());
- SettingsModel.HeroImageContentType = file.ContentType;
+ var processedFile = file.ContentType.Equals("image/svg+xml", StringComparison.OrdinalIgnoreCase)
+ ? file
+ : await file.RequestImageFileAsync(targetContentType, maxHeroImageWidth, maxHeroImageHeight).ConfigureAwait(false);
+
+ if (processedFile.Size > maxProcessedFileSize)
+ {
+ HeroImageUploadMessage = "Image is still too large after optimization. Please choose a smaller image.";
+ return;
+ }
+
+ var webRootPath = WebHostEnvironment.WebRootPath;
+ if (string.IsNullOrWhiteSpace(webRootPath))
+ {
+ HeroImageUploadMessage = "Unable to resolve web root path for image storage.";
+ return;
+ }
+
+ var uploadsFolder = Path.Combine(webRootPath, "uploads", "registry-hero");
+ Directory.CreateDirectory(uploadsFolder);
+
+ var extension = GetHeroImageExtension(processedFile.ContentType);
+ var fileName = $"{RegistryId:N}-{Guid.NewGuid():N}{extension}";
+ var relativePath = $"/uploads/registry-hero/{fileName}";
+ var fullPath = Path.Combine(uploadsFolder, fileName);
+
+ await using (var destinationStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ await using (var sourceStream = processedFile.OpenReadStream(maxProcessedFileSize))
+ {
+ await sourceStream.CopyToAsync(destinationStream).ConfigureAwait(false);
+ }
+
+ DeleteManagedHeroImage(SettingsModel.HeroImagePath);
+
+ SettingsModel.HeroImagePath = relativePath;
+ SettingsModel.HeroImageBase64 = null;
+ SettingsModel.HeroImageContentType = null;
HeroImageUploadMessage = "Image uploaded successfully. Click 'Save Settings' to apply.";
}
@@ -848,10 +889,41 @@ public partial class RegistryAdmin : ComponentBase
protected Task RemoveHeroImageAsync()
{
+ DeleteManagedHeroImage(SettingsModel.HeroImagePath);
+ SettingsModel.HeroImagePath = string.Empty;
SettingsModel.HeroImageBase64 = string.Empty;
SettingsModel.HeroImageContentType = null;
HeroImageUploadMessage = "Image will be removed when you save settings.";
return Task.CompletedTask;
}
+
+ private static string GetHeroImageExtension(string contentType)
+ {
+ return contentType.ToLowerInvariant() switch
+ {
+ "image/png" => ".png",
+ "image/webp" => ".webp",
+ "image/svg+xml" => ".svg",
+ _ => ".jpg"
+ };
+ }
+
+ private void DeleteManagedHeroImage(string? relativePath)
+ {
+ if (string.IsNullOrWhiteSpace(relativePath)
+ || !relativePath.StartsWith("/uploads/registry-hero/", StringComparison.OrdinalIgnoreCase)
+ || string.IsNullOrWhiteSpace(WebHostEnvironment.WebRootPath))
+ {
+ return;
+ }
+
+ var sanitizedPath = relativePath.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
+ var fullPath = Path.Combine(WebHostEnvironment.WebRootPath, sanitizedPath);
+
+ if (File.Exists(fullPath))
+ {
+ File.Delete(fullPath);
+ }
+ }
}
diff --git a/src/BirthList.Web/Components/Pages/RegistryPublic.razor b/src/BirthList.Web/Components/Pages/RegistryPublic.razor
index 74600ad..f93d848 100644
--- a/src/BirthList.Web/Components/Pages/RegistryPublic.razor
+++ b/src/BirthList.Web/Components/Pages/RegistryPublic.razor
@@ -12,10 +12,10 @@
else
{
- @if (!string.IsNullOrWhiteSpace(Registry.HeroImageBase64))
+ @if (!string.IsNullOrWhiteSpace(Registry.HeroImagePath) || !string.IsNullOrWhiteSpace(Registry.HeroImageBase64))
{
-

+
}
diff --git a/src/BirthList.Web/Features/Registries/RegistryModels.cs b/src/BirthList.Web/Features/Registries/RegistryModels.cs
index f47605b..a36c7f5 100644
--- a/src/BirthList.Web/Features/Registries/RegistryModels.cs
+++ b/src/BirthList.Web/Features/Registries/RegistryModels.cs
@@ -55,6 +55,7 @@ public sealed class RegistrySettingsEditModel
public DateOnly? BirthDate { get; set; }
public string? HeroImageBase64 { get; set; }
public string? HeroImageContentType { get; set; }
+ public string? HeroImagePath { get; set; }
public string? HeaderContentHtml { get; set; }
public string? ShippingAddress { get; set; }
public string CurrencyCode { get; set; } = "€";
@@ -86,6 +87,7 @@ public sealed class RegistryPublicViewModel
public string? BabyName { get; init; }
public string? HeroImageBase64 { get; init; }
public string? HeroImageContentType { get; init; }
+ public string? HeroImagePath { get; init; }
public string? HeaderContentHtml { get; init; }
public string? ShippingAddress { get; init; }
public string CurrencyCode { get; init; } = "€";
diff --git a/src/BirthList.Web/Features/Registries/RegistryService.cs b/src/BirthList.Web/Features/Registries/RegistryService.cs
index 0826dd3..0e7269a 100644
--- a/src/BirthList.Web/Features/Registries/RegistryService.cs
+++ b/src/BirthList.Web/Features/Registries/RegistryService.cs
@@ -301,6 +301,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
BabyName = registry.BabyName,
HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null,
HeroImageContentType = registry.HeroImageContentType,
+ HeroImagePath = registry.HeroImagePath,
HeaderContentHtml = registry.HeaderContentHtml,
ShippingAddress = registry.ShippingAddress,
CurrencyCode = registry.CurrencyCode,
@@ -341,6 +342,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
BirthDate = registry.BirthDate,
HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null,
HeroImageContentType = registry.HeroImageContentType,
+ HeroImagePath = registry.HeroImagePath,
HeaderContentHtml = registry.HeaderContentHtml,
ShippingAddress = registry.ShippingAddress,
CurrencyCode = registry.CurrencyCode,
@@ -380,15 +382,24 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
registry.BabyName = string.IsNullOrWhiteSpace(model.BabyName) ? null : model.BabyName.Trim();
registry.BirthDate = model.BirthDate;
- if (!string.IsNullOrWhiteSpace(model.HeroImageBase64))
+ if (!string.IsNullOrWhiteSpace(model.HeroImagePath))
+ {
+ registry.HeroImagePath = model.HeroImagePath.Trim();
+ registry.HeroImageData = null;
+ registry.HeroImageContentType = null;
+ }
+ else if (!string.IsNullOrWhiteSpace(model.HeroImageBase64))
{
registry.HeroImageData = Convert.FromBase64String(model.HeroImageBase64);
registry.HeroImageContentType = model.HeroImageContentType;
+ registry.HeroImagePath = null;
}
- else if (string.IsNullOrWhiteSpace(model.HeroImageBase64) && model.HeroImageBase64 == string.Empty)
+ else if ((string.IsNullOrWhiteSpace(model.HeroImageBase64) && model.HeroImageBase64 == string.Empty)
+ || (string.IsNullOrWhiteSpace(model.HeroImagePath) && model.HeroImagePath == string.Empty))
{
registry.HeroImageData = null;
registry.HeroImageContentType = null;
+ registry.HeroImagePath = null;
}
registry.HeaderContentHtml = string.IsNullOrWhiteSpace(model.HeaderContentHtml) ? null : model.HeaderContentHtml;