Add disk-based hero image storage with path support
Build and Push Docker Image / build-and-push (push) Successful in 36s

Added HeroImagePath to Registries and updated models, UI, and RegistryService to support storing hero images on disk. Images are now saved to /uploads/registry-hero/ and old images are cleaned up on update/removal. Includes migration for new column. Backward compatibility with base64 BLOBs is maintained.
This commit is contained in:
Arne Moerman
2026-07-25 19:08:24 +02:00
parent 78405c6e75
commit cb073ee3d8
9 changed files with 694 additions and 13 deletions
@@ -9,6 +9,7 @@ public class Registry
public string? BabyName { get; set; } public string? BabyName { get; set; }
public byte[]? HeroImageData { get; set; } public byte[]? HeroImageData { get; set; }
public string? HeroImageContentType { get; set; } public string? HeroImageContentType { get; set; }
public string? HeroImagePath { get; set; }
public string? HeaderContentHtml { get; set; } public string? HeaderContentHtml { get; set; }
public string? ShippingAddress { get; set; } public string? ShippingAddress { get; set; }
public string CurrencyCode { get; set; } = "EUR"; public string CurrencyCode { get; set; } = "EUR";
@@ -0,0 +1,564 @@
// <auto-generated />
using System;
using BirthList.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace BirthList.Infrastructure.Migrations
{
[DbContext(typeof(RegistryDbContext))]
[Migration("20260725170215_AddHeroImagePathToRegistry")]
partial class AddHeroImagePathToRegistry
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.26")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTimeOffset>("ContributedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<string>("CurrencyCode")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("nvarchar(3)");
b.Property<Guid>("RegistryItemId")
.HasColumnType("uniqueidentifier");
b.Property<string>("TransferMessage")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RegistryItemId");
b.ToTable("ItemContributions");
});
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("PurchasedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<Guid>("RegistryItemId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RegistryItemId");
b.ToTable("ItemPurchases");
});
modelBuilder.Entity("BirthList.Domain.Entities.PlatformOwner", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("AssignedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("PlatformOwners");
});
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("BabyName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateOnly?>("BirthDate")
.HasColumnType("date");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<string>("CurrencyCode")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("nvarchar(3)");
b.Property<string>("HeaderContentHtml")
.HasColumnType("nvarchar(max)");
b.Property<string>("HeroImageContentType")
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("HeroImageData")
.HasColumnType("varbinary(max)");
b.Property<string>("HeroImagePath")
.HasColumnType("nvarchar(max)");
b.Property<string>("PublicLinkCode")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("RegistryType")
.HasColumnType("int");
b.Property<string>("ShippingAddress")
.HasColumnType("nvarchar(max)");
b.Property<string>("ThemeKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.HasKey("Id");
b.HasIndex("PublicLinkCode")
.IsUnique();
b.ToTable("Registries");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
{
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserId")
.HasMaxLength(450)
.HasColumnType("nvarchar(450)");
b.Property<DateTimeOffset>("AddedAtUtc")
.HasColumnType("datetimeoffset");
b.HasKey("RegistryId", "UserId");
b.ToTable("RegistryAdmins");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("ExpiresAtUtc")
.HasColumnType("datetimeoffset");
b.Property<DateTimeOffset?>("RedeemedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<string>("SentToEmail")
.HasMaxLength(320)
.HasColumnType("nvarchar(320)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("nvarchar(120)");
b.HasKey("Id");
b.HasIndex("RegistryId");
b.HasIndex("Token")
.IsUnique();
b.ToTable("RegistryAdminInvites");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("CategoryId")
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<string>("CurrencyCode")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("nvarchar(3)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.Property<int>("DesiredQuantity")
.HasColumnType("int");
b.Property<bool>("IsGiven")
.HasColumnType("bit");
b.Property<decimal>("MoneyFulfilledAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(300)
.HasColumnType("nvarchar(300)");
b.Property<bool>("ParticipationAllowed")
.HasColumnType("bit");
b.Property<decimal?>("ParticipationTargetAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("PictureUrl")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<bool?>("PreferSecondHand")
.HasColumnType("bit");
b.Property<decimal?>("PriceAmount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("ProductUrl")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<int>("PurchasedQuantity")
.HasColumnType("int");
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("RegistryId");
b.HasIndex("CategoryId", "SortOrder");
b.ToTable("RegistryItems");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItemCategory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("RegistryId", "Name");
b.HasIndex("RegistryId", "SortOrder");
b.ToTable("RegistryItemCategories");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
{
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<string>("BankAccountBic")
.HasMaxLength(11)
.HasColumnType("nvarchar(11)");
b.Property<string>("BankAccountDisplayName")
.HasMaxLength(120)
.HasColumnType("nvarchar(120)");
b.Property<string>("BankAccountIban")
.HasMaxLength(34)
.HasColumnType("nvarchar(34)");
b.Property<string>("ContributionAmountQrCodesJson")
.HasMaxLength(4000)
.HasColumnType("nvarchar(4000)");
b.Property<string>("ContributionQrCodeUrl")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<bool>("HideHeaderName")
.HasColumnType("bit");
b.Property<bool>("ShowBankAccountName")
.HasColumnType("bit");
b.HasKey("RegistryId");
b.ToTable("RegistrySettings");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
{
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserId")
.HasMaxLength(450)
.HasColumnType("nvarchar(450)");
b.Property<DateTimeOffset>("LastVisitedAtUtc")
.HasColumnType("datetimeoffset");
b.HasKey("RegistryId", "UserId");
b.ToTable("RegistryVisits");
});
modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("ActionType")
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTimeOffset>("CreatedAtUtc")
.HasColumnType("datetimeoffset");
b.Property<string>("Details")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<Guid>("RegistryId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("RegistryItemId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("RegistryId");
b.ToTable("UserActionLogs");
});
modelBuilder.Entity("BirthList.Domain.Entities.ItemContribution", b =>
{
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
.WithMany("Contributions")
.HasForeignKey("RegistryItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RegistryItem");
});
modelBuilder.Entity("BirthList.Domain.Entities.ItemPurchase", b =>
{
b.HasOne("BirthList.Domain.Entities.RegistryItem", "RegistryItem")
.WithMany("Purchases")
.HasForeignKey("RegistryItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("RegistryItem");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdmin", b =>
{
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithMany("Admins")
.HasForeignKey("RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b =>
{
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithMany("AdminInvites")
.HasForeignKey("RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
{
b.HasOne("BirthList.Domain.Entities.RegistryItemCategory", "Category")
.WithMany("Items")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithMany("Items")
.HasForeignKey("RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItemCategory", b =>
{
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithMany("ItemCategories")
.HasForeignKey("RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b =>
{
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithOne()
.HasForeignKey("BirthList.Domain.Entities.RegistrySettings", "RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b =>
{
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithMany("Visits")
.HasForeignKey("RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b =>
{
b.HasOne("BirthList.Domain.Entities.Registry", "Registry")
.WithMany("ActionLogs")
.HasForeignKey("RegistryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Registry");
});
modelBuilder.Entity("BirthList.Domain.Entities.Registry", b =>
{
b.Navigation("ActionLogs");
b.Navigation("AdminInvites");
b.Navigation("Admins");
b.Navigation("ItemCategories");
b.Navigation("Items");
b.Navigation("Visits");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b =>
{
b.Navigation("Contributions");
b.Navigation("Purchases");
});
modelBuilder.Entity("BirthList.Domain.Entities.RegistryItemCategory", b =>
{
b.Navigation("Items");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BirthList.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddHeroImagePathToRegistry : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HeroImagePath",
table: "Registries",
type: "nvarchar(max)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HeroImagePath",
table: "Registries");
}
}
}
@@ -141,6 +141,9 @@ namespace BirthList.Infrastructure.Migrations
b.Property<byte[]>("HeroImageData") b.Property<byte[]>("HeroImageData")
.HasColumnType("varbinary(max)"); .HasColumnType("varbinary(max)");
b.Property<string>("HeroImagePath")
.HasColumnType("nvarchar(max)");
b.Property<string>("PublicLinkCode") b.Property<string>("PublicLinkCode")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
@@ -349,12 +349,12 @@ else
<div class="mt-3"> <div class="mt-3">
<label class="form-label">@L["RegistryAdmin.HeroImage"]</label> <label class="form-label">@L["RegistryAdmin.HeroImage"]</label>
@if (!string.IsNullOrWhiteSpace(SettingsModel.HeroImageBase64)) @if (!string.IsNullOrWhiteSpace(SettingsModel.HeroImagePath) || !string.IsNullOrWhiteSpace(SettingsModel.HeroImageBase64))
{ {
<div class="mb-2"> <div class="mb-2">
<label class="form-label small">@L["RegistryAdmin.CurrentHeroImage"]</label> <label class="form-label small">@L["RegistryAdmin.CurrentHeroImage"]</label>
<div> <div>
<img src="data:@SettingsModel.HeroImageContentType;base64,@SettingsModel.HeroImageBase64" alt="Hero" class="img-thumbnail" style="max-height: 200px;" /> <img src="@(string.IsNullOrWhiteSpace(SettingsModel.HeroImagePath) ? $"data:{SettingsModel.HeroImageContentType};base64,{SettingsModel.HeroImageBase64}" : SettingsModel.HeroImagePath)" alt="Hero" class="img-thumbnail" style="max-height: 200px;" />
</div> </div>
<button type="button" class="btn btn-sm btn-outline-danger mt-2" @onclick="RemoveHeroImageAsync">@L["RegistryAdmin.RemoveHeroImage"]</button> <button type="button" class="btn btn-sm btn-outline-danger mt-2" @onclick="RemoveHeroImageAsync">@L["RegistryAdmin.RemoveHeroImage"]</button>
</div> </div>
@@ -5,6 +5,7 @@ using BirthList.Web.Services;
using Blazored.TextEditor; using Blazored.TextEditor;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@@ -23,6 +24,7 @@ public partial class RegistryAdmin : ComponentBase
[Inject] private NavigationManager NavigationManager { get; set; } = null!; [Inject] private NavigationManager NavigationManager { get; set; } = null!;
[Inject] private SmtpEmailSender EmailSender { get; set; } = null!; [Inject] private SmtpEmailSender EmailSender { get; set; } = null!;
[Inject] private SmtpConfigurationStatusService SmtpConfigurationStatusService { get; set; } = null!; [Inject] private SmtpConfigurationStatusService SmtpConfigurationStatusService { get; set; } = null!;
[Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!;
[Inject] private IJSRuntime JSRuntime { get; set; } = null!; [Inject] private IJSRuntime JSRuntime { get; set; } = null!;
protected RegistrySettingsEditModel SettingsModel { get; } = new(); protected RegistrySettingsEditModel SettingsModel { get; } = new();
@@ -817,12 +819,16 @@ public partial class RegistryAdmin : ComponentBase
try 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; 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; return;
} }
@@ -832,11 +838,46 @@ public partial class RegistryAdmin : ComponentBase
return; return;
} }
using var memoryStream = new MemoryStream(); var targetContentType = file.ContentType.Equals("image/png", StringComparison.OrdinalIgnoreCase)
await file.OpenReadStream(maxFileSize).CopyToAsync(memoryStream).ConfigureAwait(false); ? "image/png"
: "image/jpeg";
SettingsModel.HeroImageBase64 = Convert.ToBase64String(memoryStream.ToArray()); var processedFile = file.ContentType.Equals("image/svg+xml", StringComparison.OrdinalIgnoreCase)
SettingsModel.HeroImageContentType = file.ContentType; ? 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."; HeroImageUploadMessage = "Image uploaded successfully. Click 'Save Settings' to apply.";
} }
@@ -848,10 +889,41 @@ public partial class RegistryAdmin : ComponentBase
protected Task RemoveHeroImageAsync() protected Task RemoveHeroImageAsync()
{ {
DeleteManagedHeroImage(SettingsModel.HeroImagePath);
SettingsModel.HeroImagePath = string.Empty;
SettingsModel.HeroImageBase64 = string.Empty; SettingsModel.HeroImageBase64 = string.Empty;
SettingsModel.HeroImageContentType = null; SettingsModel.HeroImageContentType = null;
HeroImageUploadMessage = "Image will be removed when you save settings."; HeroImageUploadMessage = "Image will be removed when you save settings.";
return Task.CompletedTask; 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);
}
}
} }
@@ -12,10 +12,10 @@
else else
{ {
<div class="registry-shell @RegistryThemeService.GetCssClass(Registry.RegistryType, Registry.ThemeKey)"> <div class="registry-shell @RegistryThemeService.GetCssClass(Registry.RegistryType, Registry.ThemeKey)">
@if (!string.IsNullOrWhiteSpace(Registry.HeroImageBase64)) @if (!string.IsNullOrWhiteSpace(Registry.HeroImagePath) || !string.IsNullOrWhiteSpace(Registry.HeroImageBase64))
{ {
<div class="hero-image-container mb-4"> <div class="hero-image-container mb-4">
<img src="data:@Registry.HeroImageContentType;base64,@Registry.HeroImageBase64" alt="@Registry.Title" class="hero-image img-fluid w-100" /> <img src="@(string.IsNullOrWhiteSpace(Registry.HeroImagePath) ? $"data:{Registry.HeroImageContentType};base64,{Registry.HeroImageBase64}" : Registry.HeroImagePath)" alt="@Registry.Title" class="hero-image img-fluid w-100" />
</div> </div>
} }
@@ -55,6 +55,7 @@ public sealed class RegistrySettingsEditModel
public DateOnly? BirthDate { get; set; } public DateOnly? BirthDate { get; set; }
public string? HeroImageBase64 { get; set; } public string? HeroImageBase64 { get; set; }
public string? HeroImageContentType { get; set; } public string? HeroImageContentType { get; set; }
public string? HeroImagePath { get; set; }
public string? HeaderContentHtml { get; set; } public string? HeaderContentHtml { get; set; }
public string? ShippingAddress { get; set; } public string? ShippingAddress { get; set; }
public string CurrencyCode { get; set; } = "€"; public string CurrencyCode { get; set; } = "€";
@@ -86,6 +87,7 @@ public sealed class RegistryPublicViewModel
public string? BabyName { get; init; } public string? BabyName { get; init; }
public string? HeroImageBase64 { get; init; } public string? HeroImageBase64 { get; init; }
public string? HeroImageContentType { get; init; } public string? HeroImageContentType { get; init; }
public string? HeroImagePath { get; init; }
public string? HeaderContentHtml { get; init; } public string? HeaderContentHtml { get; init; }
public string? ShippingAddress { get; init; } public string? ShippingAddress { get; init; }
public string CurrencyCode { get; init; } = "€"; public string CurrencyCode { get; init; } = "€";
@@ -301,6 +301,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
BabyName = registry.BabyName, BabyName = registry.BabyName,
HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null, HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null,
HeroImageContentType = registry.HeroImageContentType, HeroImageContentType = registry.HeroImageContentType,
HeroImagePath = registry.HeroImagePath,
HeaderContentHtml = registry.HeaderContentHtml, HeaderContentHtml = registry.HeaderContentHtml,
ShippingAddress = registry.ShippingAddress, ShippingAddress = registry.ShippingAddress,
CurrencyCode = registry.CurrencyCode, CurrencyCode = registry.CurrencyCode,
@@ -341,6 +342,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
BirthDate = registry.BirthDate, BirthDate = registry.BirthDate,
HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null, HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null,
HeroImageContentType = registry.HeroImageContentType, HeroImageContentType = registry.HeroImageContentType,
HeroImagePath = registry.HeroImagePath,
HeaderContentHtml = registry.HeaderContentHtml, HeaderContentHtml = registry.HeaderContentHtml,
ShippingAddress = registry.ShippingAddress, ShippingAddress = registry.ShippingAddress,
CurrencyCode = registry.CurrencyCode, 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.BabyName = string.IsNullOrWhiteSpace(model.BabyName) ? null : model.BabyName.Trim();
registry.BirthDate = model.BirthDate; 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.HeroImageData = Convert.FromBase64String(model.HeroImageBase64);
registry.HeroImageContentType = model.HeroImageContentType; 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.HeroImageData = null;
registry.HeroImageContentType = null; registry.HeroImageContentType = null;
registry.HeroImagePath = null;
} }
registry.HeaderContentHtml = string.IsNullOrWhiteSpace(model.HeaderContentHtml) ? null : model.HeaderContentHtml; registry.HeaderContentHtml = string.IsNullOrWhiteSpace(model.HeaderContentHtml) ? null : model.HeaderContentHtml;