Add disk-based hero image storage with path support
Build and Push Docker Image / build-and-push (push) Successful in 36s
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:
@@ -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";
|
||||||
|
|||||||
Generated
+564
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user