From b323cdd47a6b3c9faa3d788676ca37480b179d13 Mon Sep 17 00:00:00 2001 From: Arne Moerman Date: Sat, 25 Jul 2026 18:07:25 +0200 Subject: [PATCH] Add hero image support for registries - Registry entity and DB updated with hero image fields - Admin UI: upload, preview, remove hero image with validation - Public registry page displays hero image if present - View models and RegistryService handle hero image data (base64) - Localization updated for hero image UI strings - Minor: Fix ShowCreateForm property in Home.razor --- src/BirthList.Domain/Entities/Registry.cs | 2 + .../20260517000000_InitialCreate.Designer.cs | 424 +++++++++++++ .../20260517000000_InitialCreate.cs | 275 +++++++++ ...11_AddRegistryItemCategoriesAndOrdering.cs | 0 ...9130000_AddHeroImageToRegistry.Designer.cs | 559 ++++++++++++++++++ .../20260519130000_AddHeroImageToRegistry.cs | 39 ++ .../RegistryDbContextModelSnapshot.cs | 7 + src/BirthList.Web/Components/Pages/Home.razor | 3 +- .../Components/Pages/RegistryAdmin.razor | 22 + .../Components/Pages/RegistryAdmin.razor.cs | 46 ++ .../Components/Pages/RegistryPublic.razor | 7 + .../Components/Pages/RegistryPublic.razor.css | 14 + .../Features/Registries/RegistryModels.cs | 4 + .../Features/Registries/RegistryService.cs | 16 + .../Resources/SharedResources.fr-FR.resx | 4 + .../Resources/SharedResources.nl-BE.resx | 4 + .../Resources/SharedResources.qps-Ploc.resx | 12 + .../Resources/SharedResources.resx | 4 + 18 files changed, 1441 insertions(+), 1 deletion(-) create mode 100644 src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.Designer.cs create mode 100644 src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.cs delete mode 100644 src/BirthList.Infrastructure/Migrations/20260519123811_AddRegistryItemCategoriesAndOrdering.cs create mode 100644 src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.Designer.cs create mode 100644 src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.cs diff --git a/src/BirthList.Domain/Entities/Registry.cs b/src/BirthList.Domain/Entities/Registry.cs index 357b8db..ccadf7b 100644 --- a/src/BirthList.Domain/Entities/Registry.cs +++ b/src/BirthList.Domain/Entities/Registry.cs @@ -7,6 +7,8 @@ public class Registry public string Title { get; set; } = string.Empty; public DateOnly? BirthDate { get; set; } public string? BabyName { get; set; } + public byte[]? HeroImageData { get; set; } + public string? HeroImageContentType { get; set; } public string? HeaderContentHtml { get; set; } public string? ShippingAddress { get; set; } public string CurrencyCode { get; set; } = "EUR"; diff --git a/src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.Designer.cs b/src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.Designer.cs new file mode 100644 index 0000000..736ea21 --- /dev/null +++ b/src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.Designer.cs @@ -0,0 +1,424 @@ +// +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("20260517000000_InitialCreate")] + partial class InitialCreate + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ContributedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("RegistryItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("TransferMessage") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("PurchasedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RegistryItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AssignedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BabyName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("HeaderContentHtml") + .HasColumnType("nvarchar(max)"); + + b.Property("PublicLinkCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RegistryType") + .HasColumnType("int"); + + b.Property("ShippingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ThemeKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("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("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("AddedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("RegistryId", "UserId"); + + b.ToTable("RegistryAdmins"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("RedeemedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("SentToEmail") + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CanBeSecondHand") + .HasColumnType("bit"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DesiredQuantity") + .HasColumnType("int"); + + b.Property("IsGiven") + .HasColumnType("bit"); + + b.Property("MoneyFulfilledAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("ParticipationAllowed") + .HasColumnType("bit"); + + b.Property("ParticipationTargetAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("PictureUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("PriceAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProductUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("PurchasedQuantity") + .HasColumnType("int"); + + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RegistryId"); + + b.ToTable("RegistryItems"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.RegistrySettings", b => + { + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("BankAccountBic") + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("BankAccountDisplayName") + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("BankAccountIban") + .HasMaxLength(34) + .HasColumnType("nvarchar(34)"); + + b.HasKey("RegistryId"); + + b.ToTable("RegistrySettings"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b => + { + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("LastVisitedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("RegistryId", "UserId"); + + b.ToTable("RegistryVisits"); + }); + + 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.Registry", "Registry") + .WithMany("Items") + .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.Registry", b => + { + b.Navigation("AdminInvites"); + + b.Navigation("Admins"); + + b.Navigation("Items"); + + b.Navigation("Visits"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.RegistryItem", b => + { + b.Navigation("Contributions"); + + b.Navigation("Purchases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.cs b/src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.cs new file mode 100644 index 0000000..1b0f045 --- /dev/null +++ b/src/BirthList.Infrastructure/Migrations/20260517000000_InitialCreate.cs @@ -0,0 +1,275 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BirthList.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PlatformOwners", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + AssignedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PlatformOwners", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Registries", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Title = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false), + PublicLinkCode = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + BabyName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + BirthDate = table.Column(type: "date", nullable: true), + RegistryType = table.Column(type: "int", nullable: false), + HeaderContentHtml = table.Column(type: "nvarchar(max)", nullable: true), + ShippingAddress = table.Column(type: "nvarchar(max)", nullable: true), + CurrencyCode = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + ThemeKey = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + CreatedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Registries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "RegistryAdmins", + columns: table => new + { + RegistryId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + AddedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegistryAdmins", x => new { x.RegistryId, x.UserId }); + table.ForeignKey( + name: "FK_RegistryAdmins_Registries_RegistryId", + column: x => x.RegistryId, + principalTable: "Registries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RegistryAdminInvites", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + RegistryId = table.Column(type: "uniqueidentifier", nullable: false), + Token = table.Column(type: "nvarchar(120)", maxLength: 120, nullable: false), + SentToEmail = table.Column(type: "nvarchar(320)", maxLength: 320, nullable: true), + ExpiresAtUtc = table.Column(type: "datetimeoffset", nullable: false), + RedeemedAtUtc = table.Column(type: "datetimeoffset", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RegistryAdminInvites", x => x.Id); + table.ForeignKey( + name: "FK_RegistryAdminInvites_Registries_RegistryId", + column: x => x.RegistryId, + principalTable: "Registries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RegistryItems", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + RegistryId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: true), + PictureUrl = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), + ProductUrl = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), + CurrencyCode = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + PriceAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true), + DesiredQuantity = table.Column(type: "int", nullable: false), + PurchasedQuantity = table.Column(type: "int", nullable: false), + ParticipationAllowed = table.Column(type: "bit", nullable: false), + ParticipationTargetAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true), + MoneyFulfilledAmount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + CanBeSecondHand = table.Column(type: "bit", nullable: false), + IsGiven = table.Column(type: "bit", nullable: false), + CreatedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegistryItems", x => x.Id); + table.ForeignKey( + name: "FK_RegistryItems_Registries_RegistryId", + column: x => x.RegistryId, + principalTable: "Registries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RegistrySettings", + columns: table => new + { + RegistryId = table.Column(type: "uniqueidentifier", nullable: false), + BankAccountIban = table.Column(type: "nvarchar(34)", maxLength: 34, nullable: true), + BankAccountBic = table.Column(type: "nvarchar(11)", maxLength: 11, nullable: true), + BankAccountDisplayName = table.Column(type: "nvarchar(120)", maxLength: 120, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RegistrySettings", x => x.RegistryId); + table.ForeignKey( + name: "FK_RegistrySettings_Registries_RegistryId", + column: x => x.RegistryId, + principalTable: "Registries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RegistryVisits", + columns: table => new + { + RegistryId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + LastVisitedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RegistryVisits", x => new { x.RegistryId, x.UserId }); + table.ForeignKey( + name: "FK_RegistryVisits_Registries_RegistryId", + column: x => x.RegistryId, + principalTable: "Registries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ItemContributions", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + RegistryItemId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + Amount = table.Column(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false), + CurrencyCode = table.Column(type: "nvarchar(3)", maxLength: 3, nullable: false), + TransferMessage = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + ContributedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemContributions", x => x.Id); + table.ForeignKey( + name: "FK_ItemContributions_RegistryItems_RegistryItemId", + column: x => x.RegistryItemId, + principalTable: "RegistryItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ItemPurchases", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + RegistryItemId = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: false), + Quantity = table.Column(type: "int", nullable: false), + PurchasedAtUtc = table.Column(type: "datetimeoffset", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ItemPurchases", x => x.Id); + table.ForeignKey( + name: "FK_ItemPurchases_RegistryItems_RegistryItemId", + column: x => x.RegistryItemId, + principalTable: "RegistryItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ItemContributions_RegistryItemId", + table: "ItemContributions", + column: "RegistryItemId"); + + migrationBuilder.CreateIndex( + name: "IX_ItemPurchases_RegistryItemId", + table: "ItemPurchases", + column: "RegistryItemId"); + + migrationBuilder.CreateIndex( + name: "IX_PlatformOwners_UserId", + table: "PlatformOwners", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Registries_PublicLinkCode", + table: "Registries", + column: "PublicLinkCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RegistryAdminInvites_RegistryId", + table: "RegistryAdminInvites", + column: "RegistryId"); + + migrationBuilder.CreateIndex( + name: "IX_RegistryAdminInvites_Token", + table: "RegistryAdminInvites", + column: "Token", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RegistryItems_RegistryId", + table: "RegistryItems", + column: "RegistryId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ItemContributions"); + + migrationBuilder.DropTable( + name: "ItemPurchases"); + + migrationBuilder.DropTable( + name: "PlatformOwners"); + + migrationBuilder.DropTable( + name: "RegistryAdminInvites"); + + migrationBuilder.DropTable( + name: "RegistryAdmins"); + + migrationBuilder.DropTable( + name: "RegistryItems"); + + migrationBuilder.DropTable( + name: "RegistrySettings"); + + migrationBuilder.DropTable( + name: "RegistryVisits"); + + migrationBuilder.DropTable( + name: "Registries"); + } + } +} diff --git a/src/BirthList.Infrastructure/Migrations/20260519123811_AddRegistryItemCategoriesAndOrdering.cs b/src/BirthList.Infrastructure/Migrations/20260519123811_AddRegistryItemCategoriesAndOrdering.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.Designer.cs b/src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.Designer.cs new file mode 100644 index 0000000..7cf40e0 --- /dev/null +++ b/src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.Designer.cs @@ -0,0 +1,559 @@ +// +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("20260519130000_AddHeroImageToRegistry")] + partial class AddHeroImageToRegistry + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ContributedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("RegistryItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("TransferMessage") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("PurchasedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RegistryItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AssignedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BabyName") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("HeaderContentHtml") + .HasColumnType("nvarchar(max)"); + + b.Property("HeroImageContentType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("HeroImageData") + .HasColumnType("varbinary(max)"); + + b.Property("PublicLinkCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RegistryType") + .HasColumnType("int"); + + b.Property("ShippingAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("ThemeKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("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("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("AddedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("RegistryId", "UserId"); + + b.ToTable("RegistryAdmins"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.RegistryAdminInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("RedeemedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("SentToEmail") + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("CurrencyCode") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("nvarchar(3)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DesiredQuantity") + .HasColumnType("int"); + + b.Property("IsGiven") + .HasColumnType("bit"); + + b.Property("MoneyFulfilledAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)"); + + b.Property("ParticipationAllowed") + .HasColumnType("bit"); + + b.Property("ParticipationTargetAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("PictureUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("PreferSecondHand") + .HasColumnType("bit"); + + b.Property("PriceAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ProductUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("PurchasedQuantity") + .HasColumnType("int"); + + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("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("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("BankAccountBic") + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("BankAccountDisplayName") + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("BankAccountIban") + .HasMaxLength(34) + .HasColumnType("nvarchar(34)"); + + b.Property("ContributionAmountQrCodesJson") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("ContributionQrCodeUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("ShowBankAccountName") + .HasColumnType("bit"); + + b.HasKey("RegistryId"); + + b.ToTable("RegistrySettings"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.RegistryVisit", b => + { + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("LastVisitedAtUtc") + .HasColumnType("datetimeoffset"); + + b.HasKey("RegistryId", "UserId"); + + b.ToTable("RegistryVisits"); + }); + + modelBuilder.Entity("BirthList.Domain.Entities.UserActionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActionType") + .HasColumnType("int"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAtUtc") + .HasColumnType("datetimeoffset"); + + b.Property("Details") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Quantity") + .HasColumnType("int"); + + b.Property("RegistryId") + .HasColumnType("uniqueidentifier"); + + b.Property("RegistryItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("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 + } + } +} diff --git a/src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.cs b/src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.cs new file mode 100644 index 0000000..ef476c6 --- /dev/null +++ b/src/BirthList.Infrastructure/Migrations/20260519130000_AddHeroImageToRegistry.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace BirthList.Infrastructure.Migrations +{ + /// + public partial class AddHeroImageToRegistry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HeroImageData", + table: "Registries", + type: "varbinary(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "HeroImageContentType", + table: "Registries", + type: "nvarchar(100)", + maxLength: 100, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HeroImageData", + table: "Registries"); + + migrationBuilder.DropColumn( + name: "HeroImageContentType", + table: "Registries"); + } + } +} diff --git a/src/BirthList.Infrastructure/Migrations/RegistryDbContextModelSnapshot.cs b/src/BirthList.Infrastructure/Migrations/RegistryDbContextModelSnapshot.cs index 2af21f5..56313d3 100644 --- a/src/BirthList.Infrastructure/Migrations/RegistryDbContextModelSnapshot.cs +++ b/src/BirthList.Infrastructure/Migrations/RegistryDbContextModelSnapshot.cs @@ -135,6 +135,13 @@ namespace BirthList.Infrastructure.Migrations b.Property("HeaderContentHtml") .HasColumnType("nvarchar(max)"); + b.Property("HeroImageContentType") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("HeroImageData") + .HasColumnType("varbinary(max)"); + b.Property("PublicLinkCode") .IsRequired() .HasMaxLength(100) diff --git a/src/BirthList.Web/Components/Pages/Home.razor b/src/BirthList.Web/Components/Pages/Home.razor index d14dcc0..f5ed4ed 100644 --- a/src/BirthList.Web/Components/Pages/Home.razor +++ b/src/BirthList.Web/Components/Pages/Home.razor @@ -1,4 +1,5 @@ @page "/" +@rendermode InteractiveServer @using BirthList.Web.Features.Registries @@ -121,7 +122,7 @@ protected IReadOnlyList MyRegistries { get; private set; } = []; protected IReadOnlyList VisitedRegistries { get; private set; } = []; protected string? ErrorMessage { get; private set; } - protected bool ShowCreateForm { get; private set; } + protected bool ShowCreateForm { get; set; } [Inject] private RegistryService RegistryService { get; set; } = null!; [Inject] private RegistryUserContext RegistryUserContext { get; set; } = null!; diff --git a/src/BirthList.Web/Components/Pages/RegistryAdmin.razor b/src/BirthList.Web/Components/Pages/RegistryAdmin.razor index dca7a1b..ee40c78 100644 --- a/src/BirthList.Web/Components/Pages/RegistryAdmin.razor +++ b/src/BirthList.Web/Components/Pages/RegistryAdmin.razor @@ -338,6 +338,28 @@ else @L["RegistryAdmin.LineBreaksPreserved"] +
+ + @if (!string.IsNullOrWhiteSpace(SettingsModel.HeroImageBase64)) + { +
+ +
+ Hero +
+ +
+ } +
+ + + @if (!string.IsNullOrWhiteSpace(HeroImageUploadMessage)) + { + @HeroImageUploadMessage + } +
+
+
diff --git a/src/BirthList.Web/Components/Pages/RegistryAdmin.razor.cs b/src/BirthList.Web/Components/Pages/RegistryAdmin.razor.cs index fa7cea4..d46679f 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.Components.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; @@ -55,6 +56,7 @@ public partial class RegistryAdmin : ComponentBase protected string? InviteLink { get; private set; } protected BlazoredTextEditor? TextEditor { get; set; } protected string ActiveTab { get; set; } = "items"; + protected string? HeroImageUploadMessage { get; set; } protected RenderFragment ToolbarContent => builder => { builder.AddMarkupContent(0, @""); @@ -805,4 +807,48 @@ public partial class RegistryAdmin : ComponentBase return false; } } + + protected async Task OnHeroImageSelectedAsync(InputFileChangeEventArgs e) + { + HeroImageUploadMessage = null; + + try + { + const long maxFileSize = 10 * 1024 * 1024; // 10 MB + var file = e.File; + + if (file.Size > maxFileSize) + { + HeroImageUploadMessage = $"File size exceeds maximum of 10 MB"; + return; + } + + if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) + { + HeroImageUploadMessage = "Only image files are allowed"; + return; + } + + using var memoryStream = new MemoryStream(); + await file.OpenReadStream(maxFileSize).CopyToAsync(memoryStream).ConfigureAwait(false); + + SettingsModel.HeroImageBase64 = Convert.ToBase64String(memoryStream.ToArray()); + SettingsModel.HeroImageContentType = file.ContentType; + + HeroImageUploadMessage = "Image uploaded successfully. Click 'Save Settings' to apply."; + } + catch (Exception ex) + { + HeroImageUploadMessage = $"Error uploading image: {ex.Message}"; + } + } + + protected Task RemoveHeroImageAsync() + { + SettingsModel.HeroImageBase64 = string.Empty; + SettingsModel.HeroImageContentType = null; + HeroImageUploadMessage = "Image will be removed when you save settings."; + return Task.CompletedTask; + } } + diff --git a/src/BirthList.Web/Components/Pages/RegistryPublic.razor b/src/BirthList.Web/Components/Pages/RegistryPublic.razor index 608e076..0c45bea 100644 --- a/src/BirthList.Web/Components/Pages/RegistryPublic.razor +++ b/src/BirthList.Web/Components/Pages/RegistryPublic.razor @@ -12,6 +12,13 @@ else {
+ @if (!string.IsNullOrWhiteSpace(Registry.HeroImageBase64)) + { +
+ @Registry.Title +
+ } +

@(string.IsNullOrWhiteSpace(Registry.BabyName) ? Registry.Title : Registry.BabyName)

@if (!string.IsNullOrWhiteSpace(Registry.HeaderContentHtml)) diff --git a/src/BirthList.Web/Components/Pages/RegistryPublic.razor.css b/src/BirthList.Web/Components/Pages/RegistryPublic.razor.css index 0a91bd5..10153a3 100644 --- a/src/BirthList.Web/Components/Pages/RegistryPublic.razor.css +++ b/src/BirthList.Web/Components/Pages/RegistryPublic.razor.css @@ -4,6 +4,20 @@ gap: 1rem; } +.hero-image-container { + margin: 0 -1rem; + max-height: 400px; + overflow: hidden; + border-radius: 8px; +} + +.hero-image { + object-fit: cover; + object-position: center; + max-height: 400px; + border-radius: 8px; +} + .item-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); diff --git a/src/BirthList.Web/Features/Registries/RegistryModels.cs b/src/BirthList.Web/Features/Registries/RegistryModels.cs index 73daa0e..e4c7254 100644 --- a/src/BirthList.Web/Features/Registries/RegistryModels.cs +++ b/src/BirthList.Web/Features/Registries/RegistryModels.cs @@ -53,6 +53,8 @@ public sealed class RegistrySettingsEditModel { public string? BabyName { get; set; } public DateOnly? BirthDate { get; set; } + public string? HeroImageBase64 { get; set; } + public string? HeroImageContentType { get; set; } public string? HeaderContentHtml { get; set; } public string? ShippingAddress { get; set; } public string CurrencyCode { get; set; } = "€"; @@ -80,6 +82,8 @@ public sealed class RegistryPublicViewModel public string Title { get; init; } = string.Empty; public string PublicLinkCode { get; init; } = string.Empty; public string? BabyName { get; init; } + public string? HeroImageBase64 { get; init; } + public string? HeroImageContentType { 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 bc4d043..5a93929 100644 --- a/src/BirthList.Web/Features/Registries/RegistryService.cs +++ b/src/BirthList.Web/Features/Registries/RegistryService.cs @@ -299,6 +299,8 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli Title = registry.Title, PublicLinkCode = registry.PublicLinkCode, BabyName = registry.BabyName, + HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null, + HeroImageContentType = registry.HeroImageContentType, HeaderContentHtml = registry.HeaderContentHtml, ShippingAddress = registry.ShippingAddress, CurrencyCode = registry.CurrencyCode, @@ -336,6 +338,8 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli { BabyName = registry.BabyName, BirthDate = registry.BirthDate, + HeroImageBase64 = registry.HeroImageData != null ? Convert.ToBase64String(registry.HeroImageData) : null, + HeroImageContentType = registry.HeroImageContentType, HeaderContentHtml = registry.HeaderContentHtml, ShippingAddress = registry.ShippingAddress, CurrencyCode = registry.CurrencyCode, @@ -372,6 +376,18 @@ 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)) + { + registry.HeroImageData = Convert.FromBase64String(model.HeroImageBase64); + registry.HeroImageContentType = model.HeroImageContentType; + } + else if (string.IsNullOrWhiteSpace(model.HeroImageBase64) && model.HeroImageBase64 == string.Empty) + { + registry.HeroImageData = null; + registry.HeroImageContentType = null; + } + registry.HeaderContentHtml = string.IsNullOrWhiteSpace(model.HeaderContentHtml) ? null : model.HeaderContentHtml; registry.ShippingAddress = string.IsNullOrWhiteSpace(model.ShippingAddress) ? null : model.ShippingAddress.Trim(); registry.CurrencyCode = NormalizeCurrencySymbol(model.CurrencyCode); diff --git a/src/BirthList.Web/Resources/SharedResources.fr-FR.resx b/src/BirthList.Web/Resources/SharedResources.fr-FR.resx index 7ec44a5..46041ef 100644 --- a/src/BirthList.Web/Resources/SharedResources.fr-FR.resx +++ b/src/BirthList.Web/Resources/SharedResources.fr-FR.resx @@ -175,6 +175,10 @@ Moderne Adresse de livraison Les sauts de ligne seront conservés + Image héros + Télécharger une image + Supprimer l'image héros + Image héros actuelle Contenu d'en-tête Texte d'accueil Paramètres du compte bancaire diff --git a/src/BirthList.Web/Resources/SharedResources.nl-BE.resx b/src/BirthList.Web/Resources/SharedResources.nl-BE.resx index 06d39c0..61ce07c 100644 --- a/src/BirthList.Web/Resources/SharedResources.nl-BE.resx +++ b/src/BirthList.Web/Resources/SharedResources.nl-BE.resx @@ -175,6 +175,10 @@ Modern Leveringsadres Regeleinden blijven behouden + Hero-afbeelding + Afbeelding uploaden + Hero-afbeelding verwijderen + Huidige hero-afbeelding Bovenste inhoud Welkomsttekst Bankrekeninginstellingen diff --git a/src/BirthList.Web/Resources/SharedResources.qps-Ploc.resx b/src/BirthList.Web/Resources/SharedResources.qps-Ploc.resx index 2f7d222..7b733e6 100644 --- a/src/BirthList.Web/Resources/SharedResources.qps-Ploc.resx +++ b/src/BirthList.Web/Resources/SharedResources.qps-Ploc.resx @@ -501,6 +501,18 @@ [[Line breaks will be preserved]] + + [[Hero image]] + + + [[Upload image]] + + + [[Remove hero image]] + + + [[Current hero image]] + [[Top content]] diff --git a/src/BirthList.Web/Resources/SharedResources.resx b/src/BirthList.Web/Resources/SharedResources.resx index 8408099..e3d78de 100644 --- a/src/BirthList.Web/Resources/SharedResources.resx +++ b/src/BirthList.Web/Resources/SharedResources.resx @@ -182,6 +182,10 @@ Modern Shipping address Line breaks will be preserved + Hero image + Upload image + Remove hero image + Current hero image Top content Welcome text Bank Account Settings