Compare commits
11 Commits
89c439f80b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 14fec4fefb | |||
| cb073ee3d8 | |||
| 78405c6e75 | |||
| 0152a6868a | |||
| b323cdd47a | |||
| f1186b31f9 | |||
| b1b7525759 | |||
| 50238b57c8 | |||
| 3b219da4eb | |||
| b59ad6e5ab | |||
| 9f3ae1051c |
@@ -6,11 +6,12 @@
|
||||
- Authentication must support local accounts plus Microsoft and Google login; admin invite links are single-use and not email-locked.
|
||||
- Money participation defaults to EUR and tracks fulfilled amount per item via bank-transfer contributions with item-linked messages.
|
||||
- Registry visibility is public-by-link initially, but logged-in users who visited a registry should be able to rediscover it in-app.
|
||||
- Identities must be hidden from non-admin users for privacy, only admins and the user who purchased/contributed can see that he did so. Other users may see a generic indication that a contrubution/purchase was made but never a name/email of another user.
|
||||
- Identities must be hidden from non-admin users for privacy; only admins and the user who purchased/contributed can see that he did so. Other users may see a generic indication that a contribution/purchase was made but never a name/email of another user.
|
||||
- Use SQL Server as the default provider and set `RequireConfirmedAccount=false` for MVP testing; the first registered account should be the owner with full access.
|
||||
|
||||
## Technical Specifications
|
||||
- Use SMTP via Google initially for email functionalities.
|
||||
- Support registry type theming from day one to enhance user experience.
|
||||
- URL autofetch should utilize OpenGraph/meta tags for better link previews.
|
||||
- Use Blazored.TextEditor for rich text editing.
|
||||
- Use Blazored.TextEditor for rich text editing.
|
||||
- Use centralized RESX localization with hierarchical keys (not sentence keys); prefer a shared resource file over per-component files, and use pseudo-localization (qps-Ploc) to catch hardcoded strings.
|
||||
@@ -1,4 +1,9 @@
|
||||
<Solution>
|
||||
<Folder Name="/deploy/">
|
||||
<File Path="deploy/portainer-stack.env.example" />
|
||||
<File Path="deploy/portainer-stack.yml" />
|
||||
<File Path="deploy/README.md" />
|
||||
</Folder>
|
||||
<Folder Name="/gitea build/">
|
||||
<File Path=".gitea/workflows/build-and-push.yml" />
|
||||
</Folder>
|
||||
|
||||
@@ -23,6 +23,11 @@ services:
|
||||
- Smtp__FromAddress=${SMTP_FROM_ADDRESS}
|
||||
- Smtp__FromName=${SMTP_FROM_NAME}
|
||||
- PublicUrl=${PUBLIC_URL}
|
||||
- AmazonMetadata__AccessKey=${AMAZON_PA_ACCESS_KEY}
|
||||
- AmazonMetadata__SecretKey=${AMAZON_PA_SECRET_KEY}
|
||||
- AmazonMetadata__AssociateTag=${AMAZON_ASSOCIATE_TAG}
|
||||
- AmazonMetadata__PaApiHost=${AMAZON_PA_API_HOST}
|
||||
- AmazonMetadata__RapidApiKey=${AMAZON_RAPID_API_KEY}
|
||||
depends_on:
|
||||
- mssql
|
||||
networks:
|
||||
|
||||
@@ -7,6 +7,9 @@ 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? HeroImagePath { get; set; }
|
||||
public string? HeaderContentHtml { get; set; }
|
||||
public string? ShippingAddress { get; set; }
|
||||
public string CurrencyCode { get; set; } = "EUR";
|
||||
|
||||
@@ -7,6 +7,7 @@ public class RegistrySettings
|
||||
public string? BankAccountBic { get; set; }
|
||||
public string? BankAccountDisplayName { get; set; }
|
||||
public bool ShowBankAccountName { get; set; }
|
||||
public bool HideHeaderName { get; set; }
|
||||
public string? ContributionQrCodeUrl { get; set; }
|
||||
public string? ContributionAmountQrCodesJson { get; set; }
|
||||
|
||||
|
||||
@@ -22,5 +22,7 @@ public enum UserActionType
|
||||
MarkPurchased = 3,
|
||||
UnmarkPurchased = 4,
|
||||
MarkPartialPurchase = 5,
|
||||
LogContribution = 6
|
||||
LogContribution = 6,
|
||||
MetadataFetchSucceeded = 7,
|
||||
MetadataFetchFailed = 8
|
||||
}
|
||||
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
// <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("20260517000000_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <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>("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<bool>("CanBeSecondHand")
|
||||
.HasColumnType("bit");
|
||||
|
||||
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<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.HasKey("Id");
|
||||
|
||||
b.HasIndex("RegistryId");
|
||||
|
||||
b.ToTable("RegistryItems");
|
||||
});
|
||||
|
||||
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.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.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BirthList.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PlatformOwners",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||
AssignedAtUtc = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PlatformOwners", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Registries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
|
||||
PublicLinkCode = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
BabyName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
BirthDate = table.Column<DateOnly>(type: "date", nullable: true),
|
||||
RegistryType = table.Column<int>(type: "int", nullable: false),
|
||||
HeaderContentHtml = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
ShippingAddress = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CurrencyCode = table.Column<string>(type: "nvarchar(3)", maxLength: 3, nullable: false),
|
||||
ThemeKey = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Registries", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RegistryAdmins",
|
||||
columns: table => new
|
||||
{
|
||||
RegistryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||
AddedAtUtc = table.Column<DateTimeOffset>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RegistryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Token = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
SentToEmail = table.Column<string>(type: "nvarchar(320)", maxLength: 320, nullable: true),
|
||||
ExpiresAtUtc = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
RedeemedAtUtc = table.Column<DateTimeOffset>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RegistryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PictureUrl = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
|
||||
ProductUrl = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
|
||||
CurrencyCode = table.Column<string>(type: "nvarchar(3)", maxLength: 3, nullable: false),
|
||||
PriceAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true),
|
||||
DesiredQuantity = table.Column<int>(type: "int", nullable: false),
|
||||
PurchasedQuantity = table.Column<int>(type: "int", nullable: false),
|
||||
ParticipationAllowed = table.Column<bool>(type: "bit", nullable: false),
|
||||
ParticipationTargetAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true),
|
||||
MoneyFulfilledAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
CanBeSecondHand = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsGiven = table.Column<bool>(type: "bit", nullable: false),
|
||||
CreatedAtUtc = table.Column<DateTimeOffset>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BankAccountIban = table.Column<string>(type: "nvarchar(34)", maxLength: 34, nullable: true),
|
||||
BankAccountBic = table.Column<string>(type: "nvarchar(11)", maxLength: 11, nullable: true),
|
||||
BankAccountDisplayName = table.Column<string>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||
LastVisitedAtUtc = table.Column<DateTimeOffset>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RegistryItemId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||
Amount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
CurrencyCode = table.Column<string>(type: "nvarchar(3)", maxLength: 3, nullable: false),
|
||||
TransferMessage = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
ContributedAtUtc = table.Column<DateTimeOffset>(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<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RegistryItemId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
|
||||
Quantity = table.Column<int>(type: "int", nullable: false),
|
||||
PurchasedAtUtc = table.Column<DateTimeOffset>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
// <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("20260519130000_AddHeroImageToRegistry")]
|
||||
partial class AddHeroImageToRegistry
|
||||
{
|
||||
/// <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")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<byte[]>("HeroImageData")
|
||||
.HasColumnType("varbinary(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>("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,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BirthList.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHeroImageToRegistry : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "HeroImageData",
|
||||
table: "Registries",
|
||||
type: "varbinary(max)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "HeroImageContentType",
|
||||
table: "Registries",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HeroImageData",
|
||||
table: "Registries");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HeroImageContentType",
|
||||
table: "Registries");
|
||||
}
|
||||
}
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
// <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("20260725163539_AddHideHeaderNameToRegistrySettings")]
|
||||
partial class AddHideHeaderNameToRegistrySettings
|
||||
{
|
||||
/// <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>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BirthList.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHideHeaderNameToRegistrySettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "HideHeaderName",
|
||||
table: "RegistrySettings",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "HeroImageContentType",
|
||||
table: "Registries",
|
||||
type: "nvarchar(max)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(100)",
|
||||
oldMaxLength: 100,
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HideHeaderName",
|
||||
table: "RegistrySettings");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "HeroImageContentType",
|
||||
table: "Registries",
|
||||
type: "nvarchar(100)",
|
||||
maxLength: 100,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "nvarchar(max)",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,15 @@ namespace BirthList.Infrastructure.Migrations
|
||||
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)
|
||||
@@ -344,6 +353,9 @@ namespace BirthList.Infrastructure.Migrations
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("nvarchar(2048)");
|
||||
|
||||
b.Property<bool>("HideHeaderName")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("ShowBankAccountName")
|
||||
.HasColumnType("bit");
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</main>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
@L["MainLayout.UnhandledError"]
|
||||
<a href="" class="reload">@L["MainLayout.Reload"]</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">BirthList</a>
|
||||
<a class="navbar-brand" href="">@L["NavMenu.Brand"]</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
|
||||
<input type="checkbox" title="@L["NavMenu.NavigationMenu"]" class="navbar-toggler" />
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> @L["NavMenu.Home"]
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<AntiforgeryToken />
|
||||
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
|
||||
<button type="submit" class="nav-link">
|
||||
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
|
||||
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> @L["NavMenu.Logout"]
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -38,12 +38,12 @@
|
||||
<NotAuthorized>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Account/Register">
|
||||
<span class="bi bi-person-nav-menu" aria-hidden="true"></span> Register
|
||||
<span class="bi bi-person-nav-menu" aria-hidden="true"></span> @L["NavMenu.Register"]
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Account/Login">
|
||||
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> Login
|
||||
<span class="bi bi-person-badge-nav-menu" aria-hidden="true"></span> @L["NavMenu.Login"]
|
||||
</NavLink>
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
|
||||
@@ -1,48 +1,64 @@
|
||||
@using System.Globalization
|
||||
@using BirthList.Web.Features.Localization
|
||||
@using BirthList.Web.Features.Registries
|
||||
@using BirthList.Web.Services
|
||||
@using Microsoft.Extensions.Hosting
|
||||
@implements IDisposable
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject RegistryUserContext RegistryUserContext
|
||||
@inject ProfileCompletionService ProfileCompletionService
|
||||
@inject IHostEnvironment HostEnvironment
|
||||
|
||||
@if (ShowProfileCompletionPrompt)
|
||||
{
|
||||
<div class="profile-completion-banner">
|
||||
<span>Please complete your profile (first name, last name, and address).</span>
|
||||
<a href="/Account/Manage" class="profile-completion-link">Complete profile</a>
|
||||
<span>@L["TopBar.ProfilePrompt"]</span>
|
||||
<a href="/Account/Manage" class="profile-completion-link">@L["TopBar.CompleteProfile"]</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<nav class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<a href="/" class="top-bar-brand" title="Gift List">
|
||||
<a href="/" class="top-bar-brand" title="@L["TopBar.Brand"]">
|
||||
<i class="bi bi-gift" aria-hidden="true"></i>
|
||||
<span class="brand-text">Gift List</span>
|
||||
<span class="brand-text">@L["TopBar.Brand"]</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="top-bar-right">
|
||||
<form action="/set-language" method="post" class="d-inline-flex align-items-center me-2">
|
||||
<AntiforgeryToken />
|
||||
<input type="hidden" name="returnUrl" value="@currentAbsolutePath" />
|
||||
<label for="language-picker" class="visually-hidden">@L["TopBar.Language"]</label>
|
||||
<select id="language-picker" name="culture" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
@foreach (var option in LanguageOptions)
|
||||
{
|
||||
<option value="@option.Culture" selected="@(string.Equals(option.Culture, CurrentCulture, StringComparison.OrdinalIgnoreCase))">@option.DisplayName</option>
|
||||
}
|
||||
</select>
|
||||
</form>
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="user-info">
|
||||
<i class="bi bi-person-circle" aria-hidden="true"></i>
|
||||
<span class="user-email">@context.User.Identity?.Name</span>
|
||||
</div>
|
||||
<a href="Account/Manage" class="top-bar-link" title="Account settings">
|
||||
<a href="Account/Manage" class="top-bar-link" title="@L["TopBar.AccountSettings"]">
|
||||
<i class="bi bi-gear" aria-hidden="true"></i>
|
||||
</a>
|
||||
<form action="Account/Logout" method="post" class="logout-form">
|
||||
<AntiforgeryToken />
|
||||
<input type="hidden" name="ReturnUrl" value="@currentUrl" />
|
||||
<button type="submit" class="top-bar-link logout-btn" title="Sign out">
|
||||
<button type="submit" class="top-bar-link logout-btn" title="@L["TopBar.SignOut"]">
|
||||
<i class="bi bi-box-arrow-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="Account/Login" class="top-bar-link" title="Sign in">
|
||||
<a href="Account/Login" class="top-bar-link" title="@L["TopBar.SignIn"]">
|
||||
<i class="bi bi-box-arrow-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</NotAuthorized>
|
||||
@@ -52,11 +68,33 @@
|
||||
|
||||
@code {
|
||||
private string currentUrl = "";
|
||||
private string currentAbsolutePath = "/";
|
||||
private bool ShowProfileCompletionPrompt { get; set; }
|
||||
private string CurrentCulture { get; set; } = "en";
|
||||
|
||||
private IReadOnlyList<LanguageOption> LanguageOptions => HostEnvironment.IsDevelopment()
|
||||
? DevelopmentLanguageOptions
|
||||
: ProductionLanguageOptions;
|
||||
|
||||
private static readonly IReadOnlyList<LanguageOption> ProductionLanguageOptions =
|
||||
[
|
||||
new("en", "English"),
|
||||
new("nl-NL", "Nederlands (NL)"),
|
||||
new("nl-BE", "Nederlands (BE)"),
|
||||
new("fr-FR", "Français")
|
||||
];
|
||||
|
||||
private static readonly IReadOnlyList<LanguageOption> DevelopmentLanguageOptions =
|
||||
[
|
||||
..ProductionLanguageOptions,
|
||||
new("qps-Ploc", "Pseudo")
|
||||
];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
currentUrl = GetRelativePath(NavigationManager.Uri);
|
||||
currentAbsolutePath = BuildAbsolutePath(NavigationManager.Uri);
|
||||
CurrentCulture = CultureInfo.CurrentUICulture.Name;
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
|
||||
await RefreshProfileCompletionPromptAsync().ConfigureAwait(false);
|
||||
@@ -65,6 +103,8 @@
|
||||
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
currentUrl = GetRelativePath(e.Location);
|
||||
currentAbsolutePath = BuildAbsolutePath(e.Location);
|
||||
CurrentCulture = CultureInfo.CurrentUICulture.Name;
|
||||
await RefreshProfileCompletionPromptAsync().ConfigureAwait(false);
|
||||
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
|
||||
}
|
||||
@@ -99,6 +139,19 @@
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
private string BuildAbsolutePath(string absoluteUri)
|
||||
{
|
||||
if (!Uri.TryCreate(absoluteUri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return "/";
|
||||
}
|
||||
|
||||
var pathAndQuery = uri.PathAndQuery;
|
||||
return string.IsNullOrWhiteSpace(pathAndQuery) ? "/" : pathAndQuery;
|
||||
}
|
||||
|
||||
private sealed record LanguageOption(string Culture, string DisplayName);
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
<PageTitle>@L["Error.PageTitle"]</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
<h1 class="text-danger">@L["Error.Title"]</h1>
|
||||
<h2 class="text-danger">@L["Error.Subtitle"]</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
<strong>@L["Error.RequestId"]</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<h3>@L["Error.DevMode"]</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
@L["Error.DevHint1"]
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
<strong>@L["Error.DevHint2"]</strong>
|
||||
@L["Error.DevHint3"]
|
||||
@L["Error.DevHint4"]
|
||||
</p>
|
||||
|
||||
@code{
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using BirthList.Web.Features.Registries
|
||||
|
||||
<PageTitle>Birth Registry</PageTitle>
|
||||
<PageTitle>@L["Home.PageTitle"]</PageTitle>
|
||||
|
||||
<h1>Welcome to Gift List</h1>
|
||||
<h1>@L["Home.Welcome"]</h1>
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized Context="authState">
|
||||
<div class="registry-sections">
|
||||
<div class="mb-4">
|
||||
<div class="section-header">
|
||||
<h2>Registries you manage</h2>
|
||||
<h2>@L["Home.ManagedRegistries"]</h2>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => ShowCreateForm = !ShowCreateForm">
|
||||
<span class="bi bi-plus"></span> Create new
|
||||
<span class="bi bi-plus"></span> @L["Home.CreateNew"]
|
||||
</button>
|
||||
</div>
|
||||
@if (MyRegistries.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No registries yet.</p>
|
||||
<p class="text-muted">@L["Home.NoRegistries"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -28,8 +29,8 @@
|
||||
<div class="registry-card">
|
||||
<h3>@registry.Title</h3>
|
||||
<div class="registry-actions">
|
||||
<a href="/registry/@registry.PublicLinkCode" class="btn btn-outline-primary btn-sm">View</a>
|
||||
<a href="/registry/@registry.Id/admin" class="btn btn-outline-secondary btn-sm">Manage</a>
|
||||
<a href="/registry/@registry.PublicLinkCode" class="btn btn-outline-primary btn-sm">@L["Home.View"]</a>
|
||||
<a href="/registry/@registry.Id/admin" class="btn btn-outline-secondary btn-sm">@L["Home.Manage"]</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -38,10 +39,10 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h2>Visited registries</h2>
|
||||
<h2>@L["Home.VisitedRegistries"]</h2>
|
||||
@if (VisitedRegistries.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No visited registries yet.</p>
|
||||
<p class="text-muted">@L["Home.NoVisitedRegistries"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -51,7 +52,7 @@
|
||||
<div class="registry-card">
|
||||
<h3>@registry.Title</h3>
|
||||
<div class="registry-actions">
|
||||
<a href="/registry/@registry.PublicLinkCode" class="btn btn-outline-primary btn-sm">View</a>
|
||||
<a href="/registry/@registry.PublicLinkCode" class="btn btn-outline-primary btn-sm">@L["Home.View"]</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -65,30 +66,30 @@
|
||||
<div class="create-registry-modal-overlay" @onclick="() => ShowCreateForm = false">
|
||||
<div class="create-registry-modal" @onclick:stopPropagation="true">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Create new registry</h3>
|
||||
<h3 class="modal-title">@L["Home.CreateRegistryTitle"]</h3>
|
||||
<button type="button" class="btn-close" @onclick="() => ShowCreateForm = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<EditForm Model="Model" OnValidSubmit="CreateRegistryAsync" Context="formContext" FormName="create-registry-form">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title</label>
|
||||
<label class="form-label">@L["Home.Title"]</label>
|
||||
<InputText class="form-control" @bind-Value="Model.Title" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Type</label>
|
||||
<label class="form-label">@L["Home.Type"]</label>
|
||||
<InputSelect class="form-select" @bind-Value="Model.RegistryType">
|
||||
<option value="@BirthList.Domain.Entities.RegistryType.Birth">Birth</option>
|
||||
<option value="@BirthList.Domain.Entities.RegistryType.Wedding">Wedding</option>
|
||||
<option value="@BirthList.Domain.Entities.RegistryType.Birthday">Birthday</option>
|
||||
<option value="@BirthList.Domain.Entities.RegistryType.Birth">@L["Home.RegistryType.Birth"]</option>
|
||||
<option value="@BirthList.Domain.Entities.RegistryType.Wedding">@L["Home.RegistryType.Wedding"]</option>
|
||||
<option value="@BirthList.Domain.Entities.RegistryType.Birthday">@L["Home.RegistryType.Birthday"]</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Theme</label>
|
||||
<label class="form-label">@L["Home.Theme"]</label>
|
||||
<InputSelect class="form-select" @bind-Value="Model.ThemeKey">
|
||||
<option value="default">Default</option>
|
||||
<option value="soft">Soft</option>
|
||||
<option value="modern">Modern</option>
|
||||
<option value="default">@L["Home.Theme.Default"]</option>
|
||||
<option value="soft">@L["Home.Theme.Soft"]</option>
|
||||
<option value="modern">@L["Home.Theme.Modern"]</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||
@@ -96,8 +97,8 @@
|
||||
<div class="alert alert-danger mb-3">@ErrorMessage</div>
|
||||
}
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="() => ShowCreateForm = false">Cancel</button>
|
||||
<button class="btn btn-primary" type="submit">Create</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="() => ShowCreateForm = false">@L["Common.Cancel"]</button>
|
||||
<button class="btn btn-primary" type="submit">@L["Home.CreateNew"]</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
@@ -107,7 +108,9 @@
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<div class="alert alert-info mt-4">
|
||||
<p>Please <a href="Account/Login">log in</a> to create and manage registries.</p>
|
||||
<p>
|
||||
@L["Home.LoginPromptPrefix"]<a href="Account/Login">@L["Home.LoginPromptLinkText"]</a>@L["Home.LoginPromptSuffix"]
|
||||
</p>
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
@@ -119,7 +122,7 @@
|
||||
protected IReadOnlyList<RegistrySummaryViewModel> MyRegistries { get; private set; } = [];
|
||||
protected IReadOnlyList<RegistrySummaryViewModel> 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!;
|
||||
@@ -136,13 +139,13 @@
|
||||
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
ErrorMessage = "You must be logged in to create a registry.";
|
||||
ErrorMessage = L["Home.MustBeLoggedIn"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Model.Title))
|
||||
{
|
||||
ErrorMessage = "Title is required.";
|
||||
ErrorMessage = L["Home.TitleRequired"];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,24 @@
|
||||
@using BirthList.Web.Features.Registries
|
||||
@using BirthList.Web.Authorization
|
||||
|
||||
<PageTitle>Action Log - Registry Admin</PageTitle>
|
||||
<PageTitle>@L["RegistryActionLog.PageTitle"]</PageTitle>
|
||||
|
||||
@if (!IsAuthorized)
|
||||
{
|
||||
<p>Access denied.</p>
|
||||
<p>@L["Common.AccessDenied"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1>Registry Action Log</h1>
|
||||
<p class="text-muted">This log shows all user actions on this registry: purchases, contributions, and other interactions.</p>
|
||||
<h1>@L["RegistryActionLog.Title"]</h1>
|
||||
<p class="text-muted">@L["RegistryActionLog.Description"]</p>
|
||||
|
||||
@if (ActionLogs is null)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
<p>@L["Common.Loading"]</p>
|
||||
}
|
||||
else if (ActionLogs.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No actions recorded yet.</p>
|
||||
<p class="text-muted">@L["RegistryActionLog.NoActions"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -29,13 +29,13 @@ else
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date/Time</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Item</th>
|
||||
<th>Quantity</th>
|
||||
<th>Amount</th>
|
||||
<th>Details</th>
|
||||
<th>@L["RegistryActionLog.DateTime"]</th>
|
||||
<th>@L["RegistryActionLog.User"]</th>
|
||||
<th>@L["RegistryActionLog.Action"]</th>
|
||||
<th>@L["RegistryActionLog.Item"]</th>
|
||||
<th>@L["RegistryActionLog.Quantity"]</th>
|
||||
<th>@L["RegistryActionLog.Amount"]</th>
|
||||
<th>@L["RegistryActionLog.Details"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -48,22 +48,28 @@ else
|
||||
@switch (log.ActionType)
|
||||
{
|
||||
case "RegistryLinkOpened":
|
||||
<span class="badge bg-info">Registry opened</span>
|
||||
<span class="badge bg-info">@L["RegistryActionLog.Badge.RegistryOpened"]</span>
|
||||
break;
|
||||
case "ItemLinkOpened":
|
||||
<span class="badge bg-info">Item link opened</span>
|
||||
<span class="badge bg-info">@L["RegistryActionLog.Badge.ItemLinkOpened"]</span>
|
||||
break;
|
||||
case "MarkPurchased":
|
||||
<span class="badge bg-success">Purchase marked</span>
|
||||
<span class="badge bg-success">@L["RegistryActionLog.Badge.PurchaseMarked"]</span>
|
||||
break;
|
||||
case "UnmarkPurchased":
|
||||
<span class="badge bg-warning">Purchase unmarked</span>
|
||||
<span class="badge bg-warning">@L["RegistryActionLog.Badge.PurchaseUnmarked"]</span>
|
||||
break;
|
||||
case "MarkPartialPurchase":
|
||||
<span class="badge bg-primary">Partial purchase</span>
|
||||
<span class="badge bg-primary">@L["RegistryActionLog.Badge.PartialPurchase"]</span>
|
||||
break;
|
||||
case "LogContribution":
|
||||
<span class="badge bg-secondary">Contribution logged</span>
|
||||
<span class="badge bg-secondary">@L["RegistryActionLog.Badge.ContributionLogged"]</span>
|
||||
break;
|
||||
case "MetadataFetchSucceeded":
|
||||
<span class="badge bg-success">@L["RegistryActionLog.Badge.MetadataFetchSucceeded"]</span>
|
||||
break;
|
||||
case "MetadataFetchFailed":
|
||||
<span class="badge bg-danger">@L["RegistryActionLog.Badge.MetadataFetchFailed"]</span>
|
||||
break;
|
||||
default:
|
||||
<span class="badge bg-dark">@log.ActionType</span>
|
||||
|
||||
@@ -3,20 +3,29 @@
|
||||
|
||||
@using Blazored.TextEditor
|
||||
|
||||
<PageTitle>Registry Admin</PageTitle>
|
||||
<PageTitle>@L["RegistryAdmin.PageTitle"]</PageTitle>
|
||||
|
||||
@if (!IsAuthorized)
|
||||
{
|
||||
<p>Access denied.</p>
|
||||
<p>@L["Common.AccessDenied"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1>Registry Admin</h1>
|
||||
<h1>@L["RegistryAdmin.Title"]</h1>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(PublicLinkCode))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<a href="/registry/@PublicLinkCode" class="btn btn-outline-primary">
|
||||
<span class="bi bi-eye"></span> @L["RegistryAdmin.ViewPublicList"]
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!IsSmtpConfigured)
|
||||
{
|
||||
<div class="alert alert-warning" role="alert">
|
||||
SMTP is not configured. Email features (identity emails and admin invite emails) are disabled. Configure the Smtp section in appsettings or user secrets.
|
||||
@L["RegistryAdmin.SmtpNotConfigured"]
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,7 +38,7 @@ else
|
||||
role="tab"
|
||||
aria-controls="items-content"
|
||||
aria-selected="@(ActiveTab == "items" ? "true" : "false")">
|
||||
<span class="bi bi-box-seam"></span> Items
|
||||
<span class="bi bi-box-seam"></span> @L["RegistryAdmin.Tab.Items"]
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
@@ -40,7 +49,7 @@ else
|
||||
role="tab"
|
||||
aria-controls="settings-content"
|
||||
aria-selected="@(ActiveTab == "settings" ? "true" : "false")">
|
||||
<span class="bi bi-gear"></span> Settings
|
||||
<span class="bi bi-gear"></span> @L["RegistryAdmin.Tab.Settings"]
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
@@ -51,7 +60,7 @@ else
|
||||
role="tab"
|
||||
aria-controls="admins-content"
|
||||
aria-selected="@(ActiveTab == "admins" ? "true" : "false")">
|
||||
<span class="bi bi-people"></span> Administrators
|
||||
<span class="bi bi-people"></span> @L["RegistryAdmin.Tab.Administrators"]
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
@@ -62,12 +71,12 @@ else
|
||||
role="tab"
|
||||
aria-controls="addresses-content"
|
||||
aria-selected="@(ActiveTab == "addresses" ? "true" : "false")">
|
||||
<span class="bi bi-house"></span> Addresses
|
||||
<span class="bi bi-house"></span> @L["RegistryAdmin.Tab.Addresses"]
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="/registry/@RegistryId/admin/action-log" class="nav-link">
|
||||
<span class="bi bi-clock-history"></span> Action Log
|
||||
<span class="bi bi-clock-history"></span> @L["RegistryAdmin.Tab.ActionLog"]
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -76,62 +85,62 @@ else
|
||||
<!-- Items Tab -->
|
||||
<div class="tab-pane fade @(GetTabPaneClass("items"))" id="items-content" role="tabpanel" aria-labelledby="items-tab">
|
||||
<section class="mb-4">
|
||||
<h2>Add or edit item</h2>
|
||||
<h2>@L["RegistryAdmin.AddOrEditItem"]</h2>
|
||||
<EditForm Model="ItemModel" OnValidSubmit="SaveItemAsync" FormName="registry-item-form">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Name</label>
|
||||
<label class="form-label">@L["RegistryAdmin.Name"]</label>
|
||||
<InputText class="form-control" @bind-Value="ItemModel.Name" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Product URL</label>
|
||||
<label class="form-label">@L["RegistryAdmin.ProductUrl"]</label>
|
||||
<div class="input-group">
|
||||
<InputText class="form-control" @bind-Value="ItemModel.ProductUrl" />
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="FetchMetadataAsync">Auto fetch</button>
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="FetchMetadataAsync">@L["RegistryAdmin.AutoFetch"]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Picture URL</label>
|
||||
<label class="form-label">@L["RegistryAdmin.PictureUrl"]</label>
|
||||
<InputText class="form-control" @bind-Value="ItemModel.PictureUrl" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Description</label>
|
||||
<label class="form-label">@L["RegistryAdmin.Description"]</label>
|
||||
<InputTextArea class="form-control" @bind-Value="ItemModel.Description" rows="2" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Price</label>
|
||||
<label class="form-label">@L["RegistryAdmin.Price"]</label>
|
||||
<InputNumber class="form-control" @bind-Value="ItemModel.PriceAmount" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Currency</label>
|
||||
<label class="form-label">@L["RegistryAdmin.CurrencySymbol"]</label>
|
||||
<InputText class="form-control" @bind-Value="ItemModel.CurrencyCode" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Desired qty</label>
|
||||
<label class="form-label">@L["RegistryAdmin.DesiredQty"]</label>
|
||||
<InputNumber class="form-control" @bind-Value="ItemModel.DesiredQuantity" />
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-center gap-2">
|
||||
<InputCheckbox @bind-Value="ItemModel.ParticipationAllowed" />
|
||||
<label>Participation</label>
|
||||
<label>@L["RegistryAdmin.Participation"]</label>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Participation target</label>
|
||||
<label class="form-label">@L["RegistryAdmin.ParticipationTarget"]</label>
|
||||
<InputNumber class="form-control" @bind-Value="ItemModel.ParticipationTargetAmount" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Second hand preference</label>
|
||||
<InputSelect class="form-select" @bind-Value="ItemModel.PreferSecondHand">
|
||||
<option value="">Second hand optional</option>
|
||||
<option value="true">Prefer second hand</option>
|
||||
<option value="false">New only</option>
|
||||
<label class="form-label">@L["RegistryAdmin.SecondHandPreference"]</label>
|
||||
<InputSelect class="form-select" @bind-Value="PreferSecondHandString">
|
||||
<option value="">@L["RegistryAdmin.SecondHandOptional"]</option>
|
||||
<option value="true">@L["RegistryAdmin.PreferSecondHand"]</option>
|
||||
<option value="false">@L["RegistryAdmin.NewOnly"]</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-center gap-2">
|
||||
<InputCheckbox @bind-Value="ItemModel.IsGiven" />
|
||||
<label>Given</label>
|
||||
<label>@L["RegistryAdmin.Given"]</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Category</label>
|
||||
<label class="form-label">@L["RegistryAdmin.Category"]</label>
|
||||
<InputSelect class="form-select" @bind-Value="ItemModel.CategoryId">
|
||||
@foreach (var category in ItemCategories)
|
||||
{
|
||||
@@ -140,16 +149,16 @@ else
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary mt-3" type="submit">Save item</button>
|
||||
<button class="btn btn-primary mt-3" type="submit">@L["RegistryAdmin.SaveItem"]</button>
|
||||
</EditForm>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="d-flex justify-content-between align-items-end flex-wrap gap-3">
|
||||
<h2 class="mb-0">Categories and items</h2>
|
||||
<h2 class="mb-0">@L["RegistryAdmin.CategoriesAndItems"]</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<InputText class="form-control" @bind-Value="NewCategoryName" placeholder="New category" />
|
||||
<button type="button" class="btn btn-outline-primary" @onclick="AddCategoryAsync">Add category</button>
|
||||
<InputText class="form-control" @bind-Value="NewCategoryName" placeholder="@L["RegistryAdmin.NewCategory"]" />
|
||||
<button type="button" class="btn btn-outline-primary" @onclick="AddCategoryAsync">@L["RegistryAdmin.AddCategory"]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,7 +167,7 @@ else
|
||||
<div class="alert alert-warning mt-3 mb-0" role="alert">@ItemManagementMessage</div>
|
||||
}
|
||||
|
||||
<p class="text-muted mt-3 mb-2">Drag categories or items to reorder. Drop items into another category to regroup them.</p>
|
||||
<p class="text-muted mt-3 mb-2">@L["RegistryAdmin.DragHint"]</p>
|
||||
|
||||
<div class="category-groups mt-3">
|
||||
@foreach (var category in ItemCategories)
|
||||
@@ -178,8 +187,8 @@ else
|
||||
{
|
||||
<div class="d-flex gap-2 align-items-center w-100">
|
||||
<InputText class="form-control form-control-sm" @bind-Value="CategoryRenameName" />
|
||||
<button type="button" class="btn btn-sm btn-primary" @onclick="() => SaveCategoryRenameAsync(category.Id)">Save</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="CancelCategoryRename">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" @onclick="() => SaveCategoryRenameAsync(category.Id)">@L["Common.Save"]</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="CancelCategoryRename">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -189,13 +198,13 @@ else
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
@onclick="() => StartCategoryRename(category)">
|
||||
Rename
|
||||
@L["RegistryAdmin.Rename"]
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
@onclick="() => RemoveCategoryAsync(category.Id)"
|
||||
disabled="@(ItemCategories.Count <= 1)">
|
||||
Remove
|
||||
@L["Common.Remove"]
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -208,17 +217,18 @@ else
|
||||
@ondrop="() => OnCategoryItemsDropAsync(category.Id)">
|
||||
@if (category.Items.Count == 0)
|
||||
{
|
||||
<div class="p-3 text-muted">Drop items here.</div>
|
||||
<div class="p-3 text-muted">@L["RegistryAdmin.DropItemsHere"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Desired Qty</th>
|
||||
<th>Participation</th>
|
||||
<th>Purchased by / Contributed by</th>
|
||||
<th>@L["RegistryAdmin.Header.Name"]</th>
|
||||
<th>@L["RegistryAdmin.Header.DesiredQty"]</th>
|
||||
<th>@L["RegistryAdmin.Header.Condition"]</th>
|
||||
<th>@L["RegistryAdmin.Header.Participation"]</th>
|
||||
<th>@L["RegistryAdmin.Header.PurchasedContributedBy"]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -239,12 +249,26 @@ else
|
||||
@ondrop="() => OnItemDropAsync(category.Id, item.Id!.Value)">
|
||||
<td>@item.Name</td>
|
||||
<td>@item.DesiredQuantity</td>
|
||||
<td>@(item.ParticipationAllowed ? "Yes" : "No")</td>
|
||||
<td>
|
||||
@if (item.PreferSecondHand == true)
|
||||
{
|
||||
<span class="badge bg-success">@L["RegistryAdmin.PreferSecondHand"]</span>
|
||||
}
|
||||
else if (item.PreferSecondHand == false)
|
||||
{
|
||||
<span class="badge bg-primary">@L["RegistryAdmin.NewOnly"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">@L["RegistryAdmin.SecondHandOptional"]</span>
|
||||
}
|
||||
</td>
|
||||
<td>@(item.ParticipationAllowed ? L["Common.Yes"].Value : L["Common.No"].Value)</td>
|
||||
<td>
|
||||
@if (item.Purchasers.Count > 0)
|
||||
{
|
||||
<div class="mb-1">
|
||||
<small><strong>Purchased:</strong></small>
|
||||
<small><strong>@L["RegistryAdmin.Purchased"]</strong></small>
|
||||
<div class="small">
|
||||
@foreach (var purchaser in item.Purchasers)
|
||||
{
|
||||
@@ -256,7 +280,7 @@ else
|
||||
@if (item.Contributors.Count > 0)
|
||||
{
|
||||
<div>
|
||||
<small><strong>Contributed:</strong></small>
|
||||
<small><strong>@L["RegistryAdmin.Contributed"]</strong></small>
|
||||
<div class="small">
|
||||
@foreach (var contributor in item.Contributors)
|
||||
{
|
||||
@@ -267,8 +291,8 @@ else
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" @onclick="() => EditItem(item)">Edit</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteItemAsync(item.Id)">Delete</button>
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" @onclick="() => EditItem(item)">@L["Common.Edit"]</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteItemAsync(item.Id)">@L["Common.Remove"]</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -292,57 +316,85 @@ else
|
||||
<!-- Settings Tab -->
|
||||
<div class="tab-pane fade @(GetTabPaneClass("settings"))" id="settings-content" role="tabpanel" aria-labelledby="settings-tab">
|
||||
<section class="mb-4">
|
||||
<h2>Registry Settings</h2>
|
||||
<h2>@L["RegistryAdmin.RegistrySettings"]</h2>
|
||||
<EditForm Model="SettingsModel" OnValidSubmit="SaveSettingsAsync" FormName="registry-settings-form">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Baby name</label>
|
||||
<label class="form-label">@L["RegistryAdmin.BabyName"]</label>
|
||||
<InputText class="form-control" @bind-Value="SettingsModel.BabyName" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Birth date</label>
|
||||
<label class="form-label">@L["RegistryAdmin.BirthDate"]</label>
|
||||
<InputDate class="form-control" @bind-Value="SettingsModel.BirthDate" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Currency</label>
|
||||
<label class="form-label">@L["RegistryAdmin.CurrencySymbol"]</label>
|
||||
<InputText class="form-control" @bind-Value="SettingsModel.CurrencyCode" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Theme</label>
|
||||
<label class="form-label">@L["RegistryAdmin.Theme"]</label>
|
||||
<InputSelect class="form-select" @bind-Value="SettingsModel.ThemeKey">
|
||||
<option value="default">Default</option>
|
||||
<option value="soft">Soft</option>
|
||||
<option value="modern">Modern</option>
|
||||
<option value="default">@L["RegistryAdmin.Theme.Default"]</option>
|
||||
<option value="soft">@L["RegistryAdmin.Theme.Soft"]</option>
|
||||
<option value="modern">@L["RegistryAdmin.Theme.Modern"]</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Shipping address</label>
|
||||
<label class="form-label">@L["RegistryAdmin.ShippingAddress"]</label>
|
||||
<InputTextArea class="form-control" @bind-Value="SettingsModel.ShippingAddress" rows="4" />
|
||||
<small class="form-text text-muted">Line breaks will be preserved</small>
|
||||
<small class="form-text text-muted">@L["RegistryAdmin.LineBreaksPreserved"]</small>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Top content</label>
|
||||
<div class="editor-wrapper">
|
||||
<BlazoredTextEditor @ref="TextEditor" Placeholder="Welcome text" Theme="snow" ToolbarContent="@ToolbarContent" />
|
||||
<label class="form-label">@L["RegistryAdmin.HeroImage"]</label>
|
||||
@if (!string.IsNullOrWhiteSpace(SettingsModel.HeroImagePath) || !string.IsNullOrWhiteSpace(SettingsModel.HeroImageBase64))
|
||||
{
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">@L["RegistryAdmin.CurrentHeroImage"]</label>
|
||||
<div>
|
||||
<img src="@(string.IsNullOrWhiteSpace(SettingsModel.HeroImagePath) ? $"data:{SettingsModel.HeroImageContentType};base64,{SettingsModel.HeroImageBase64}" : SettingsModel.HeroImagePath)" alt="Hero" class="img-thumbnail" style="max-height: 200px;" />
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger mt-2" @onclick="RemoveHeroImageAsync">@L["RegistryAdmin.RemoveHeroImage"]</button>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<label class="form-label small">@L["RegistryAdmin.UploadImage"]</label>
|
||||
<InputFile class="form-control" OnChange="OnHeroImageSelectedAsync" accept="image/*" />
|
||||
@if (!string.IsNullOrWhiteSpace(HeroImageUploadMessage))
|
||||
{
|
||||
<small class="text-muted">@HeroImageUploadMessage</small>
|
||||
}
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<InputCheckbox class="form-check-input" @bind-Value="SettingsModel.HideHeaderName" id="hideHeaderName" />
|
||||
<label class="form-check-label" for="hideHeaderName">
|
||||
@L["RegistryAdmin.HideHeaderName"]
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3>Bank Account Settings</h3>
|
||||
<label class="form-label">@L["RegistryAdmin.TopContent"]</label>
|
||||
<div class="editor-wrapper">
|
||||
<BlazoredTextEditor @ref="TextEditor" Placeholder="@L["RegistryAdmin.WelcomeText"]" Theme="snow" ToolbarContent="@ToolbarContent" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3>@L["RegistryAdmin.BankAccountSettings"]</h3>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Bank account name</label>
|
||||
<label class="form-label">@L["RegistryAdmin.BankAccountName"]</label>
|
||||
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountDisplayName" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">IBAN</label>
|
||||
<label class="form-label">@L["RegistryPublic.IBAN"]</label>
|
||||
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountIban" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">BIC</label>
|
||||
<label class="form-label">@L["RegistryPublic.BIC"]</label>
|
||||
<InputText class="form-control" @bind-Value="SettingsModel.BankAccountBic" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -350,31 +402,31 @@ else
|
||||
<div class="form-check">
|
||||
<InputCheckbox class="form-check-input" @bind-Value="SettingsModel.ShowBankAccountName" id="showBankName" />
|
||||
<label class="form-check-label" for="showBankName">
|
||||
Display bank account name
|
||||
@L["RegistryAdmin.DisplayBankAccountName"]
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3>Contribution Payment Options</h3>
|
||||
<h3>@L["RegistryAdmin.ContributionPaymentOptions"]</h3>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Single QR code URL</label>
|
||||
<label class="form-label">@L["RegistryAdmin.SingleQrCodeUrl"]</label>
|
||||
<InputText class="form-control" @bind-Value="SettingsModel.ContributionQrCodeUrl" />
|
||||
<small class="form-text text-muted">Optional: one QR code that donors can scan for any amount.</small>
|
||||
<small class="form-text text-muted">@L["RegistryAdmin.SingleQrHelp"]</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Amount-specific QR codes</h4>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="AddContributionAmountQrCode">Add QR amount</button>
|
||||
<h4 class="mb-0">@L["RegistryAdmin.AmountSpecificQrCodes"]</h4>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="AddContributionAmountQrCode">@L["RegistryAdmin.AddQrAmount"]</button>
|
||||
</div>
|
||||
|
||||
@if (SettingsModel.ContributionAmountQrCodes.Count == 0)
|
||||
{
|
||||
<p class="text-muted mt-2 mb-0">No amount-specific QR codes configured.</p>
|
||||
<p class="text-muted mt-2 mb-0">@L["RegistryAdmin.NoAmountSpecificQrCodes"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -383,15 +435,15 @@ else
|
||||
{
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Amount</label>
|
||||
<label class="form-label">@L["Common.Amount"]</label>
|
||||
<InputNumber class="form-control" @bind-Value="amountQr.Amount" />
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<label class="form-label">QR code URL</label>
|
||||
<label class="form-label">@L["RegistryAdmin.QrCodeUrl"]</label>
|
||||
<InputText class="form-control" @bind-Value="amountQr.QrCodeUrl" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" class="btn btn-outline-danger w-100" @onclick="() => RemoveContributionAmountQrCode(amountQr)">Remove</button>
|
||||
<button type="button" class="btn btn-outline-danger w-100" @onclick="() => RemoveContributionAmountQrCode(amountQr)">@L["Common.Remove"]</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -400,7 +452,7 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary mt-4" type="submit">Save settings</button>
|
||||
<button class="btn btn-primary mt-4" type="submit">@L["RegistryAdmin.SaveSettings"]</button>
|
||||
</EditForm>
|
||||
</section>
|
||||
</div>
|
||||
@@ -408,19 +460,19 @@ else
|
||||
<!-- Addresses Tab -->
|
||||
<div class="tab-pane fade @(GetTabPaneClass("addresses"))" id="addresses-content" role="tabpanel" aria-labelledby="addresses-tab">
|
||||
<section class="mb-4">
|
||||
<h2>User addresses</h2>
|
||||
<h2>@L["RegistryAdmin.UserAddresses"]</h2>
|
||||
@if (AccessibleUserAddresses.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No users found yet.</p>
|
||||
<p class="text-muted">@L["RegistryAdmin.NoUsersFound"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Address</th>
|
||||
<th>@L["RegistryAdmin.Name"]</th>
|
||||
<th>@L["RegistryAdmin.Email"]</th>
|
||||
<th>@L["RegistryAdmin.Address"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -441,17 +493,17 @@ else
|
||||
<!-- Admins Tab -->
|
||||
<div class="tab-pane fade @(GetTabPaneClass("admins"))" id="admins-content" role="tabpanel" aria-labelledby="admins-tab">
|
||||
<section class="mb-4">
|
||||
<h2>Current administrators</h2>
|
||||
<h2>@L["RegistryAdmin.CurrentAdministrators"]</h2>
|
||||
@if (Admins.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No admins assigned yet.</p>
|
||||
<p class="text-muted">@L["RegistryAdmin.NoAdmins"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email / Name</th>
|
||||
<th>@L["RegistryAdmin.EmailOrName"]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -461,7 +513,7 @@ else
|
||||
<tr>
|
||||
<td>@admin.DisplayName</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAdminAsync(admin.UserId)">Remove</button>
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteAdminAsync(admin.UserId)">@L["Common.Remove"]</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -471,19 +523,19 @@ else
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Invite administrator</h2>
|
||||
<h2>@L["RegistryAdmin.InviteAdministrator"]</h2>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-8">
|
||||
<InputText class="form-control" @bind-Value="InviteEmail" placeholder="optional email" />
|
||||
<InputText class="form-control" @bind-Value="InviteEmail" placeholder="@L["RegistryAdmin.OptionalEmail"]" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-outline-primary w-100" @onclick="CreateInviteAsync">Create invite</button>
|
||||
<button class="btn btn-outline-primary w-100" @onclick="CreateInviteAsync">@L["RegistryAdmin.CreateInvite"]</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(InviteLink))
|
||||
{
|
||||
<p class="mt-3">
|
||||
<strong>Invite link:</strong>
|
||||
<strong>@L["RegistryAdmin.InviteLink"]</strong>
|
||||
<br />
|
||||
<a href="@InviteLink" target="_blank">@InviteLink</a>
|
||||
</p>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using BirthList.Domain.Entities;
|
||||
using BirthList.Web.Authorization;
|
||||
using BirthList.Web.Features.Registries;
|
||||
using BirthList.Web.Services;
|
||||
using Blazored.TextEditor;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
@@ -21,10 +24,26 @@ public partial class RegistryAdmin : ComponentBase
|
||||
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
|
||||
[Inject] private SmtpEmailSender EmailSender { get; set; } = null!;
|
||||
[Inject] private SmtpConfigurationStatusService SmtpConfigurationStatusService { get; set; } = null!;
|
||||
[Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!;
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
|
||||
protected RegistrySettingsEditModel SettingsModel { get; } = new();
|
||||
protected RegistryItemEditModel ItemModel { get; private set; } = new();
|
||||
protected string PreferSecondHandString
|
||||
{
|
||||
get => ItemModel.PreferSecondHand switch
|
||||
{
|
||||
null => "",
|
||||
true => "true",
|
||||
false => "false"
|
||||
};
|
||||
set => ItemModel.PreferSecondHand = value switch
|
||||
{
|
||||
"true" => true,
|
||||
"false" => false,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
protected IReadOnlyList<RegistryItemEditModel> Items { get; private set; } = [];
|
||||
protected IReadOnlyList<RegistryItemCategoryEditModel> ItemCategories { get; private set; } = [];
|
||||
protected string? NewCategoryName { get; set; }
|
||||
@@ -35,16 +54,19 @@ public partial class RegistryAdmin : ComponentBase
|
||||
protected IReadOnlyList<RegistryAccessibleUserAddressViewModel> AccessibleUserAddresses { get; private set; } = [];
|
||||
protected bool IsAuthorized { get; private set; }
|
||||
protected bool IsSmtpConfigured { get; private set; }
|
||||
protected string? PublicLinkCode { get; private set; }
|
||||
protected string? InviteEmail { get; set; }
|
||||
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, "<span class='ql-formats'><select class='ql-header'><option selected></option><option value='1'></option><option value='2'></option></select></span>");
|
||||
builder.AddMarkupContent(1, "<span class='ql-formats'><button class='ql-bold'></button><button class='ql-italic'></button><button class='ql-underline'></button></span>");
|
||||
builder.AddMarkupContent(2, "<span class='ql-formats'><button class='ql-list' value='ordered'></button><button class='ql-list' value='bullet'></button></span>");
|
||||
builder.AddMarkupContent(3, "<span class='ql-formats'><button class='ql-link'></button><button class='ql-clean'></button></span>");
|
||||
builder.AddMarkupContent(0, @"<span class='ql-formats'><select class='ql-size'><option value='small'></option><option selected></option><option value='large'></option><option value='huge'></option></select><select class='ql-header'><option selected></option><option value='1'></option><option value='2'></option><option value='3'></option><option value='4'></option><option value='5'></option></select></span>");
|
||||
builder.AddMarkupContent(1, @"<span class='ql-formats'><button class='ql-bold'></button><button class='ql-italic'></button><button class='ql-underline'></button><button class='ql-strike'></button></span>");
|
||||
builder.AddMarkupContent(2, @"<span class='ql-formats'><select class=""ql-color""></select><select class=""ql-background""></select></span>");
|
||||
builder.AddMarkupContent(3, @"<span class='ql-formats'><button class='ql-list' value='ordered'></button><button class='ql-list' value='bullet'></button></span>");
|
||||
builder.AddMarkupContent(4, @"<span class='ql-formats'><button class='ql-link'></button><button class='ql-clean'></button></span>");
|
||||
};
|
||||
|
||||
private bool _pendingEditorLoad;
|
||||
@@ -583,41 +605,95 @@ public partial class RegistryAdmin : ComponentBase
|
||||
return;
|
||||
}
|
||||
|
||||
var metadata = await RegistryMetadataService.FetchAsync(ItemModel.ProductUrl, CancellationToken.None).ConfigureAwait(false);
|
||||
if (metadata is null)
|
||||
var userId = await RegistryUserContext.GetUserIdAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
var actorUserId = string.IsNullOrWhiteSpace(userId) ? "unknown-user" : userId;
|
||||
|
||||
async Task LogMetadataAsync(UserActionType actionType, string details)
|
||||
{
|
||||
return;
|
||||
try
|
||||
{
|
||||
await RegistryService.LogUserActionAsync(
|
||||
RegistryId,
|
||||
actorUserId,
|
||||
actionType,
|
||||
ItemModel.Id,
|
||||
TruncateLogDetails(details),
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Keep autofetch UX stable even if logging cannot be persisted.
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.NormalizedUrl))
|
||||
try
|
||||
{
|
||||
ItemModel.ProductUrl = metadata.NormalizedUrl;
|
||||
var metadata = await RegistryMetadataService.FetchAsync(ItemModel.ProductUrl, CancellationToken.None).ConfigureAwait(false);
|
||||
if (metadata is null)
|
||||
{
|
||||
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=no-metadata").ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.NormalizedUrl))
|
||||
{
|
||||
ItemModel.ProductUrl = metadata.NormalizedUrl;
|
||||
}
|
||||
|
||||
if ((string.IsNullOrWhiteSpace(ItemModel.Name) || string.Equals(ItemModel.Name, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Title))
|
||||
{
|
||||
ItemModel.Name = metadata.Title;
|
||||
}
|
||||
|
||||
if ((string.IsNullOrWhiteSpace(ItemModel.Description) || string.Equals(ItemModel.Description, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Description))
|
||||
{
|
||||
ItemModel.Description = metadata.Description;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ItemModel.PictureUrl) && !string.IsNullOrWhiteSpace(metadata.ImageUrl))
|
||||
{
|
||||
ItemModel.PictureUrl = metadata.ImageUrl;
|
||||
}
|
||||
|
||||
if (!ItemModel.PriceAmount.HasValue && metadata.PriceAmount.HasValue)
|
||||
{
|
||||
ItemModel.PriceAmount = metadata.PriceAmount;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.CurrencyCode))
|
||||
{
|
||||
ItemModel.CurrencyCode = metadata.CurrencyCode;
|
||||
}
|
||||
|
||||
var summary = $"metadata-fetch success; url={ItemModel.ProductUrl}; title={(string.IsNullOrWhiteSpace(metadata.Title) ? "-" : metadata.Title)}; image={(string.IsNullOrWhiteSpace(metadata.ImageUrl) ? "no" : "yes")}; price={(metadata.PriceAmount.HasValue ? metadata.PriceAmount.Value.ToString("0.00") : "-")}; currency={(string.IsNullOrWhiteSpace(metadata.CurrencyCode) ? "-" : metadata.CurrencyCode)}";
|
||||
await LogMetadataAsync(UserActionType.MetadataFetchSucceeded, summary).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=http; message={ex.Message}").ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=timeout-or-cancel; message={ex.Message}").ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=invalid-operation; message={ex.Message}").ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await LogMetadataAsync(UserActionType.MetadataFetchFailed, $"metadata-fetch failed; url={ItemModel.ProductUrl}; reason=unexpected-{ex.GetType().Name}; message={ex.Message}").ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TruncateLogDetails(string details)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(details))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if ((string.IsNullOrWhiteSpace(ItemModel.Name) || string.Equals(ItemModel.Name, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Title))
|
||||
{
|
||||
ItemModel.Name = metadata.Title;
|
||||
}
|
||||
|
||||
if ((string.IsNullOrWhiteSpace(ItemModel.Description) || string.Equals(ItemModel.Description, "Amazon", StringComparison.OrdinalIgnoreCase)) && !string.IsNullOrWhiteSpace(metadata.Description))
|
||||
{
|
||||
ItemModel.Description = metadata.Description;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ItemModel.PictureUrl) && !string.IsNullOrWhiteSpace(metadata.ImageUrl))
|
||||
{
|
||||
ItemModel.PictureUrl = metadata.ImageUrl;
|
||||
}
|
||||
|
||||
if (!ItemModel.PriceAmount.HasValue && metadata.PriceAmount.HasValue)
|
||||
{
|
||||
ItemModel.PriceAmount = metadata.PriceAmount;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.CurrencyCode))
|
||||
{
|
||||
ItemModel.CurrencyCode = metadata.CurrencyCode;
|
||||
}
|
||||
return details.Length <= 500 ? details : details[..500];
|
||||
}
|
||||
|
||||
protected async Task CreateInviteAsync()
|
||||
@@ -675,8 +751,10 @@ public partial class RegistryAdmin : ComponentBase
|
||||
SettingsModel.BankAccountIban = settings.BankAccountIban;
|
||||
SettingsModel.BankAccountBic = settings.BankAccountBic;
|
||||
SettingsModel.ShowBankAccountName = settings.ShowBankAccountName;
|
||||
SettingsModel.HideHeaderName = settings.HideHeaderName;
|
||||
SettingsModel.ContributionQrCodeUrl = settings.ContributionQrCodeUrl;
|
||||
SettingsModel.ContributionAmountQrCodes = settings.ContributionAmountQrCodes;
|
||||
PublicLinkCode = settings.PublicLinkCode;
|
||||
|
||||
var currentHeaderContent = SettingsModel.HeaderContentHtml ?? string.Empty;
|
||||
_pendingEditorLoad = currentHeaderContent != _lastLoadedHeaderContentHtml;
|
||||
@@ -734,4 +812,118 @@ public partial class RegistryAdmin : ComponentBase
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task OnHeroImageSelectedAsync(InputFileChangeEventArgs e)
|
||||
{
|
||||
HeroImageUploadMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
const long maxOriginalFileSize = 10 * 1024 * 1024; // 10 MB
|
||||
const long maxProcessedFileSize = 3 * 1024 * 1024; // 3 MB
|
||||
const int maxHeroImageWidth = 3840;
|
||||
const int maxHeroImageHeight = 2160;
|
||||
|
||||
var file = e.File;
|
||||
|
||||
if (file.Size > maxOriginalFileSize)
|
||||
{
|
||||
HeroImageUploadMessage = "File size exceeds maximum of 10 MB";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HeroImageUploadMessage = "Only image files are allowed";
|
||||
return;
|
||||
}
|
||||
|
||||
var targetContentType = file.ContentType.Equals("image/png", StringComparison.OrdinalIgnoreCase)
|
||||
? "image/png"
|
||||
: "image/jpeg";
|
||||
|
||||
var processedFile = file.ContentType.Equals("image/svg+xml", StringComparison.OrdinalIgnoreCase)
|
||||
? file
|
||||
: await file.RequestImageFileAsync(targetContentType, maxHeroImageWidth, maxHeroImageHeight).ConfigureAwait(false);
|
||||
|
||||
if (processedFile.Size > maxProcessedFileSize)
|
||||
{
|
||||
HeroImageUploadMessage = "Image is still too large after optimization. Please choose a smaller image.";
|
||||
return;
|
||||
}
|
||||
|
||||
var webRootPath = WebHostEnvironment.WebRootPath;
|
||||
if (string.IsNullOrWhiteSpace(webRootPath))
|
||||
{
|
||||
HeroImageUploadMessage = "Unable to resolve web root path for image storage.";
|
||||
return;
|
||||
}
|
||||
|
||||
var uploadsFolder = Path.Combine(webRootPath, "uploads", "registry-hero");
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
|
||||
var extension = GetHeroImageExtension(processedFile.ContentType);
|
||||
var fileName = $"{RegistryId:N}-{Guid.NewGuid():N}{extension}";
|
||||
var relativePath = $"/uploads/registry-hero/{fileName}";
|
||||
var fullPath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
await using (var destinationStream = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
await using (var sourceStream = processedFile.OpenReadStream(maxProcessedFileSize))
|
||||
{
|
||||
await sourceStream.CopyToAsync(destinationStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
DeleteManagedHeroImage(SettingsModel.HeroImagePath);
|
||||
|
||||
SettingsModel.HeroImagePath = relativePath;
|
||||
SettingsModel.HeroImageBase64 = null;
|
||||
SettingsModel.HeroImageContentType = null;
|
||||
|
||||
HeroImageUploadMessage = "Image uploaded successfully. Click 'Save Settings' to apply.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HeroImageUploadMessage = $"Error uploading image: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
protected Task RemoveHeroImageAsync()
|
||||
{
|
||||
DeleteManagedHeroImage(SettingsModel.HeroImagePath);
|
||||
SettingsModel.HeroImagePath = string.Empty;
|
||||
SettingsModel.HeroImageBase64 = string.Empty;
|
||||
SettingsModel.HeroImageContentType = null;
|
||||
HeroImageUploadMessage = "Image will be removed when you save settings.";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string GetHeroImageExtension(string contentType)
|
||||
{
|
||||
return contentType.ToLowerInvariant() switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/webp" => ".webp",
|
||||
"image/svg+xml" => ".svg",
|
||||
_ => ".jpg"
|
||||
};
|
||||
}
|
||||
|
||||
private void DeleteManagedHeroImage(string? relativePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(relativePath)
|
||||
|| !relativePath.StartsWith("/uploads/registry-hero/", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.IsNullOrWhiteSpace(WebHostEnvironment.WebRootPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sanitizedPath = relativePath.TrimStart('/').Replace('/', Path.DirectorySeparatorChar);
|
||||
var fullPath = Path.Combine(WebHostEnvironment.WebRootPath, sanitizedPath);
|
||||
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
|
||||
@using BirthList.Web.Features.Registries
|
||||
|
||||
<PageTitle>Contribution Amount</PageTitle>
|
||||
<PageTitle>@L["RegistryContributionAmount.PageTitle"]</PageTitle>
|
||||
|
||||
@if (Registry is null || Item is null)
|
||||
{
|
||||
<p>Item not found.</p>
|
||||
<p>@L["RegistryContributionAmount.ItemNotFound"]</p>
|
||||
}
|
||||
else if (!IsAuthenticated)
|
||||
{
|
||||
<p>Please log in first.</p>
|
||||
<p>@L["RegistryContributionAmount.LoginFirst"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="registry-shell @RegistryThemeService.GetCssClass(Registry.RegistryType, Registry.ThemeKey)">
|
||||
<h1>Partially fulfill: @Item.Name</h1>
|
||||
<h1>@L["RegistryContributionAmount.PartiallyFulfill", Item.Name]</h1>
|
||||
|
||||
<div class="card p-3">
|
||||
@if (RepresentableAmounts.Count == 0)
|
||||
{
|
||||
<p class="text-muted mb-0">No representable amount can be formed from configured QR codes up to €200.</p>
|
||||
<p class="text-muted mb-0">@L["RegistryContributionAmount.NoRepresentableAmount", GetCurrencySymbol()]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<label class="form-label">Select amount: €@SelectedAmount</label>
|
||||
<label class="form-label">@L["RegistryContributionAmount.SelectAmount", GetCurrencySymbol(), SelectedAmount]</label>
|
||||
<input type="range"
|
||||
class="form-range"
|
||||
min="0"
|
||||
@@ -37,15 +37,15 @@ else
|
||||
|
||||
@if (Suggestions.Count == 0)
|
||||
{
|
||||
<p class="text-muted mt-2">No QR combination available for this amount.</p>
|
||||
<p class="text-muted mt-2">@L["RegistryContributionAmount.NoQrCombination"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="mt-3 mb-2"><strong>Suggested QR combination:</strong></p>
|
||||
<p class="mt-3 mb-2"><strong>@L["RegistryContributionAmount.SuggestedCombination"]</strong></p>
|
||||
<ul class="mb-3">
|
||||
@foreach (var suggestion in Suggestions)
|
||||
{
|
||||
<li>@suggestion.RepeatCount x €@suggestion.Amount</li>
|
||||
<li>@suggestion.RepeatCount x @GetCurrencySymbol()@suggestion.Amount</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -55,10 +55,10 @@ else
|
||||
@for (var i = 0; i < suggestion.RepeatCount; i++)
|
||||
{
|
||||
<div>
|
||||
<div class="small mb-1">€@suggestion.Amount</div>
|
||||
<div class="small mb-1">@GetCurrencySymbol()@suggestion.Amount</div>
|
||||
<img src="@BuildQrImageUrl(suggestion.QrCodeUrl)" alt="QR code @suggestion.Amount" class="img-fluid" style="max-height: 220px;" />
|
||||
<div class="small text-muted text-break">
|
||||
<a href="@suggestion.QrCodeUrl" target="_blank" rel="noopener noreferrer">Open payment link</a>
|
||||
<a href="@suggestion.QrCodeUrl" target="_blank" rel="noopener noreferrer">@L["RegistryContributionAmount.OpenPaymentLink"]</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -67,8 +67,8 @@ else
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success" @onclick="ConfirmAsync" disabled="@(SelectedAmount <= 0)">I transferred this amount</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="BackToRegistry">Back</button>
|
||||
<button class="btn btn-success" @onclick="ConfirmAsync" disabled="@(SelectedAmount <= 0)">@L["RegistryContributionAmount.TransferredAmount"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="BackToRegistry">@L["Common.Back"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -207,4 +207,11 @@ public partial class RegistryContributionAmount : ComponentBase
|
||||
public string QrCodeUrl { get; init; } = string.Empty;
|
||||
public int RepeatCount { get; init; }
|
||||
}
|
||||
|
||||
protected string GetCurrencySymbol()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(Registry?.CurrencyCode)
|
||||
? "€"
|
||||
: Registry.CurrencyCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
@page "/registry/{RegistryId:guid}/invite/{Token}"
|
||||
|
||||
<PageTitle>Admin Invite</PageTitle>
|
||||
<PageTitle>@L["RegistryInvite.PageTitle"]</PageTitle>
|
||||
|
||||
<h1>Admin invitation</h1>
|
||||
<h1>@L["RegistryInvite.Title"]</h1>
|
||||
|
||||
@if (Redeemed is null)
|
||||
{
|
||||
<p>Validating invitation...</p>
|
||||
<p>@L["RegistryInvite.Validating"]</p>
|
||||
}
|
||||
else if (Redeemed.Value)
|
||||
{
|
||||
<p>Invitation accepted. You are now an admin.</p>
|
||||
<a class="btn btn-primary" href="/registry/@RegistryId/admin">Go to admin</a>
|
||||
<p>@L["RegistryInvite.Accepted"]</p>
|
||||
<a class="btn btn-primary" href="/registry/@RegistryId/admin">@L["RegistryInvite.GoToAdmin"]</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>The invitation is invalid or already used.</p>
|
||||
<p>@L["RegistryInvite.Invalid"]</p>
|
||||
}
|
||||
|
||||
@@ -3,16 +3,35 @@
|
||||
|
||||
@using BirthList.Web.Features.Registries
|
||||
|
||||
<PageTitle>Registry</PageTitle>
|
||||
<PageTitle>@L["RegistryPublic.PageTitle"]</PageTitle>
|
||||
|
||||
@if (Registry is null)
|
||||
{
|
||||
<p>Registry not found.</p>
|
||||
<p>@L["RegistryPublic.NotFound"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="registry-shell @RegistryThemeService.GetCssClass(Registry.RegistryType, Registry.ThemeKey)">
|
||||
<h1>@(string.IsNullOrWhiteSpace(Registry.BabyName) ? Registry.Title : Registry.BabyName)</h1>
|
||||
@if (!string.IsNullOrWhiteSpace(Registry.HeroImagePath) || !string.IsNullOrWhiteSpace(Registry.HeroImageBase64))
|
||||
{
|
||||
<div class="hero-image-container mb-4">
|
||||
<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>
|
||||
}
|
||||
|
||||
@if (!Registry.HideHeaderName)
|
||||
{
|
||||
<h1>@(string.IsNullOrWhiteSpace(Registry.BabyName) ? Registry.Title : Registry.BabyName)</h1>
|
||||
}
|
||||
|
||||
@if (Registry.IsAdmin)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<a href="/registry/@Registry.Id/admin" class="btn btn-outline-secondary">
|
||||
<span class="bi bi-gear"></span> @L["RegistryPublic.GoToAdmin"]
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Registry.HeaderContentHtml))
|
||||
{
|
||||
@@ -22,7 +41,7 @@ else
|
||||
@if (!string.IsNullOrWhiteSpace(Registry.ShippingAddress))
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<strong>Shipping address</strong><br />
|
||||
<strong>@L["RegistryPublic.ShippingAddress"]</strong><br />
|
||||
<span class="shipping-address-text">@Registry.ShippingAddress</span>
|
||||
</div>
|
||||
}
|
||||
@@ -30,18 +49,18 @@ else
|
||||
@if (Registry.ShowBankAccountName && !string.IsNullOrWhiteSpace(Registry.BankAccountIban))
|
||||
{
|
||||
<div class="alert alert-secondary">
|
||||
<strong>Bank transfer participation</strong><br />
|
||||
<strong>@L["RegistryPublic.BankTransferParticipation"]</strong><br />
|
||||
@Registry.BankAccountDisplayName<br />
|
||||
IBAN: @Registry.BankAccountIban
|
||||
@L["RegistryPublic.IBAN"]: @Registry.BankAccountIban
|
||||
@if (!string.IsNullOrWhiteSpace(Registry.BankAccountBic))
|
||||
{
|
||||
<span> | BIC: @Registry.BankAccountBic</span>
|
||||
<span> | @L["RegistryPublic.BIC"]: @Registry.BankAccountBic</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="category-list">
|
||||
@foreach (var category in Registry.Categories)
|
||||
@foreach (var category in Registry.Categories.Where(x => x.Items.Count > 0))
|
||||
{
|
||||
var isCollapsed = IsCategoryCollapsed(category.Id);
|
||||
<section class="category-section">
|
||||
@@ -66,11 +85,11 @@ else
|
||||
<h5 class="card-title">@item.Name</h5>
|
||||
@if (item.PreferSecondHand == true)
|
||||
{
|
||||
<span class="badge bg-info">Second-hand preferred</span>
|
||||
<span class="badge bg-info">@L["RegistryPublic.SecondHandPreferred"]</span>
|
||||
}
|
||||
else if (item.PreferSecondHand == null)
|
||||
{
|
||||
<span class="badge bg-warning">Second-hand optional</span>
|
||||
<span class="badge bg-warning">@L["RegistryPublic.SecondHandOptional"]</span>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(item.Description))
|
||||
@@ -79,15 +98,26 @@ else
|
||||
}
|
||||
@if (item.DesiredQuantity > 1)
|
||||
{
|
||||
<p class="mb-1"><strong>Qty:</strong> @item.PurchasedQuantity/@item.DesiredQuantity purchased</p>
|
||||
<p class="mb-1"><strong>@L["RegistryPublic.Qty"]</strong> @item.PurchasedQuantity/@item.DesiredQuantity @L["RegistryPublic.PurchasedSuffix"]</p>
|
||||
}
|
||||
@if (item.PriceAmount.HasValue)
|
||||
{
|
||||
<p class="mb-1"><strong>Price:</strong> @item.PriceAmount.Value.ToString("0.00") @item.CurrencyCode</p>
|
||||
<p class="mb-1"><strong>@L["RegistryPublic.Price"]</strong> @item.PriceAmount.Value.ToString("0.00") @item.CurrencyCode</p>
|
||||
}
|
||||
@if (item.ParticipationAllowed && GetParticipationTotalAmount(item).HasValue)
|
||||
{
|
||||
<p class="mb-2"><strong>Participation:</strong> €@item.MoneyFulfilledAmount.ToString("0.00") out of €@GetParticipationTotalAmount(item)!.Value.ToString("0.00") fulfilled</p>
|
||||
<p class="mb-2">
|
||||
<strong>@L["RegistryPublic.Participation"]</strong>
|
||||
@item.CurrencyCode@item.MoneyFulfilledAmount.ToString("0.00")
|
||||
@if (GetParticipationTotalAmount(item)!.Value > 0)
|
||||
{
|
||||
<span> @L["RegistryPublic.OutOfFulfilled", item.CurrencyCode, GetParticipationTotalAmount(item)!.Value.ToString("0.00")]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span> @L["RegistryPublic.FulfilledOnly"]</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (item.Purchasers.Count > 0 || item.Contributors.Count > 0)
|
||||
@@ -98,7 +128,7 @@ else
|
||||
<div class="mb-2">
|
||||
@if (item.CanViewPurchasers && Registry.IsAdmin)
|
||||
{
|
||||
<strong class="text-sm">Purchased by:</strong>
|
||||
<strong class="text-sm">@L["RegistryPublic.PurchasedBy"]</strong>
|
||||
<div class="contributor-list">
|
||||
@foreach (var purchaser in item.Purchasers)
|
||||
{
|
||||
@@ -108,18 +138,19 @@ else
|
||||
}
|
||||
else if (item.CanViewPurchasers && item.CurrentUserPurchasedQuantity > 0)
|
||||
{
|
||||
<strong class="text-sm">Purchased by:</strong>
|
||||
<strong class="text-sm">@L["RegistryPublic.PurchasedBy"]</strong>
|
||||
<div class="contributor-list">
|
||||
<span class="contributor-badge">You (@item.CurrentUserPurchasedQuantity)</span>
|
||||
<span class="contributor-badge">@L["RegistryPublic.YouQuantity", item.CurrentUserPurchasedQuantity]</span>
|
||||
@if (item.Purchasers.Count > 1)
|
||||
{
|
||||
<span class="contributor-badge">and @(item.Purchasers.Count - 1) other @(item.Purchasers.Count - 1 == 1 ? "person" : "people")</span>
|
||||
var otherCount = item.Purchasers.Count - 1;
|
||||
<span class="contributor-badge">@L["RegistryPublic.AndOtherPeople", otherCount, otherCount == 1 ? L["RegistryPublic.Person"].Value : L["RegistryPublic.People"].Value]</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted small">Purchased</span>
|
||||
<span class="text-muted small">@L["RegistryPublic.Purchased"]</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -128,7 +159,7 @@ else
|
||||
<div class="mb-2">
|
||||
@if (Registry.IsAdmin)
|
||||
{
|
||||
<strong class="text-sm">Contributed by:</strong>
|
||||
<strong class="text-sm">@L["RegistryPublic.ContributedBy"]</strong>
|
||||
<div class="contributor-list">
|
||||
@foreach (var contributor in item.Contributors)
|
||||
{
|
||||
@@ -141,23 +172,23 @@ else
|
||||
var currentUserContribution = item.Contributors.FirstOrDefault(x => x.UserId == Registry.CurrentUserId);
|
||||
if (currentUserContribution is not null)
|
||||
{
|
||||
<strong class="text-sm">Contributed by:</strong>
|
||||
<strong class="text-sm">@L["RegistryPublic.ContributedBy"]</strong>
|
||||
<div class="contributor-list">
|
||||
<span class="contributor-badge">@currentUserContribution.DisplayName (@currentUserContribution.Amount.ToString("0.00") @item.CurrencyCode)</span>
|
||||
@if (item.Contributors.Count > 1)
|
||||
{
|
||||
<span class="contributor-badge">and others</span>
|
||||
<span class="contributor-badge">@L["RegistryPublic.AndOthers"]</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted small">Contributed</span>
|
||||
<span class="text-muted small">@L["RegistryPublic.Contributed"]</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted small">Contributed</span>
|
||||
<span class="text-muted small">@L["RegistryPublic.Contributed"]</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -166,24 +197,24 @@ else
|
||||
|
||||
@if (!IsAuthenticated)
|
||||
{
|
||||
<button class="btn btn-outline-primary btn-sm" @onclick="() => LoginRedirect()">Login to purchase</button>
|
||||
<button class="btn btn-outline-primary btn-sm" @onclick="() => LoginRedirect()">@L["RegistryPublic.LoginToPurchase"]</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
@if (!string.IsNullOrWhiteSpace(item.ProductUrl) && item.CurrentUserPurchasedQuantity == 0)
|
||||
{
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => OpenPurchasePrompt(item.Id, openTab: true)">Purchase</button>
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => OpenPurchasePrompt(item.Id, openTab: true)">@L["RegistryPublic.Purchase"]</button>
|
||||
}
|
||||
|
||||
<button class="btn btn-success btn-sm" @onclick="() => OpenPurchaseManagementPromptAsync(item.Id)">
|
||||
@(Registry.IsAdmin ? "Manage purchases" : (item.CurrentUserPurchasedQuantity > 0 ? "Edit purchase" : "Mark purchased"))
|
||||
@(Registry.IsAdmin ? L["RegistryPublic.ManagePurchases"].Value : (item.CurrentUserPurchasedQuantity > 0 ? L["RegistryPublic.EditPurchase"].Value : L["RegistryPublic.MarkPurchased"].Value))
|
||||
</button>
|
||||
|
||||
@if (item.ParticipationAllowed)
|
||||
{
|
||||
<button class="btn btn-secondary btn-sm" @onclick="() => OpenContributionActionAsync(item.Id, item.CurrentUserContributionAmount)">
|
||||
@(Registry.IsAdmin ? "Manage participations" : (item.CurrentUserContributionAmount > 0 ? "Edit participation" : "Partially fulfill"))
|
||||
@(Registry.IsAdmin ? L["RegistryPublic.ManageParticipations"].Value : (item.CurrentUserContributionAmount > 0 ? L["RegistryPublic.EditParticipation"].Value : L["RegistryPublic.PartiallyFulfill"].Value))
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -203,36 +234,20 @@ else
|
||||
{
|
||||
<div class="prompt-overlay">
|
||||
<div class="prompt-card">
|
||||
<h3>Mark as purchased</h3>
|
||||
<p>How many units did you purchase?</p>
|
||||
<h3>@L["RegistryPublic.MarkAsPurchased"]</h3>
|
||||
<p>@L["RegistryPublic.HowManyUnitsPurchased"]</p>
|
||||
<InputNumber class="form-control" @bind-Value="PurchasedQuantity" />
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(PurchaseItemUrl))
|
||||
{
|
||||
<div class="mt-3">
|
||||
<a href="@PurchaseItemUrl" target="_blank" class="btn btn-outline-primary btn-sm w-100">Open product link</a>
|
||||
<a href="@PurchaseItemUrl" target="_blank" class="btn btn-outline-primary btn-sm w-100">@L["RegistryPublic.OpenProductLink"]</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success" @onclick="ConfirmPurchaseAsync">Confirm purchase</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchasePrompt">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowContributionPrompt)
|
||||
{
|
||||
<div class="prompt-overlay">
|
||||
<div class="prompt-card">
|
||||
<h3>Log contribution</h3>
|
||||
<p>Transferred amount</p>
|
||||
<InputNumber class="form-control" @bind-Value="ContributionAmount" />
|
||||
<p class="mt-2">Message: @ContributionMessage</p>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success" @onclick="ConfirmContributionAsync">Confirm</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionPrompt">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="ConfirmPurchaseAsync">@L["RegistryPublic.ConfirmPurchase"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchasePrompt">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,20 +257,20 @@ else
|
||||
{
|
||||
<div class="prompt-overlay">
|
||||
<div class="prompt-card">
|
||||
<h3>Select purchaser to unmark</h3>
|
||||
<p>Multiple users have purchased this item. Choose which purchase to unmark:</p>
|
||||
<h3>@L["RegistryPublic.SelectPurchaserToUnmark"]</h3>
|
||||
<p>@L["RegistryPublic.MultipleUsersPurchased"]</p>
|
||||
<div class="purchaser-list">
|
||||
@foreach (var purchaser in PurchasersToUnmark)
|
||||
{
|
||||
<div class="purchaser-item">
|
||||
<span>@purchaser.DisplayName (@purchaser.Quantity)</span>
|
||||
<button class="btn btn-warning btn-sm" @onclick="() => UnmarkPurchaserAsync(purchaser.UserId)">Unmark</button>
|
||||
<button class="btn btn-warning btn-sm" @onclick="() => UnmarkPurchaserAsync(purchaser.UserId)">@L["RegistryPublic.Unmark"]</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-danger" @onclick="UnmarkAllPurchasersAsync">Unmark all</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchaserSelectionPrompt">Cancel</button>
|
||||
<button class="btn btn-danger" @onclick="UnmarkAllPurchasersAsync">@L["RegistryPublic.UnmarkAll"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchaserSelectionPrompt">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,29 +280,29 @@ else
|
||||
{
|
||||
<div class="prompt-overlay">
|
||||
<div class="prompt-card">
|
||||
<h3>Partially fulfill item</h3>
|
||||
<h3>@L["RegistryPublic.PartiallyFulfillItem"]</h3>
|
||||
|
||||
@if (PartialFulfillStep == 1)
|
||||
{
|
||||
<p>Select how you want to donate:</p>
|
||||
<p>@L["RegistryPublic.SelectDonateMethod"]</p>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
@if (HasIbanPaymentOption())
|
||||
{
|
||||
<button class="btn btn-outline-primary text-start" @onclick="() => SelectPaymentMethod(ContributionPaymentMethodType.Iban)">
|
||||
IBAN transfer
|
||||
@L["RegistryPublic.IbanTransfer"]
|
||||
</button>
|
||||
}
|
||||
@if (HasSingleQrPaymentOption())
|
||||
{
|
||||
<button class="btn btn-outline-primary text-start" @onclick="() => SelectPaymentMethod(ContributionPaymentMethodType.SingleQrCode)">
|
||||
Single QR code
|
||||
@L["RegistryPublic.SingleQrCode"]
|
||||
</button>
|
||||
}
|
||||
@if (HasAmountSpecificQrPaymentOption())
|
||||
{
|
||||
<button class="btn btn-outline-primary text-start" @onclick="() => SelectPaymentMethod(ContributionPaymentMethodType.AmountSpecificQrCode)">
|
||||
QR code per amount
|
||||
@L["RegistryPublic.QrCodePerAmount"]
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -295,10 +310,10 @@ else
|
||||
@if (SelectedPaymentMethod == ContributionPaymentMethodType.Iban && !string.IsNullOrWhiteSpace(Registry?.BankAccountIban))
|
||||
{
|
||||
<div class="alert alert-secondary mt-3 mb-0">
|
||||
<strong>IBAN:</strong> @Registry.BankAccountIban
|
||||
<strong>@L["RegistryPublic.IBAN"]:</strong> @Registry.BankAccountIban
|
||||
@if (!string.IsNullOrWhiteSpace(Registry.BankAccountBic))
|
||||
{
|
||||
<span> | <strong>BIC:</strong> @Registry.BankAccountBic</span>
|
||||
<span> | <strong>@L["RegistryPublic.BIC"]:</strong> @Registry.BankAccountBic</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -308,25 +323,25 @@ else
|
||||
<div class="mt-3 text-center">
|
||||
<img src="@BuildQrImageUrl(SelectedPaymentQrCodeUrl)" alt="Payment QR code" class="img-fluid" style="max-height: 240px;" />
|
||||
<div class="mt-2">
|
||||
<a href="@SelectedPaymentQrCodeUrl" target="_blank" rel="noopener noreferrer">Open payment link</a>
|
||||
<a href="@SelectedPaymentQrCodeUrl" target="_blank" rel="noopener noreferrer">@L["RegistryPublic.OpenPaymentLink"]</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-primary" @onclick="ContinueContributionWizard" disabled="@(SelectedPaymentMethod is null)">Next</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionPrompt">Cancel</button>
|
||||
<button class="btn btn-primary" @onclick="ContinueContributionWizard" disabled="@(SelectedPaymentMethod is null)">@L["Common.Next"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionPrompt">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
}
|
||||
else if (PartialFulfillStep == 2)
|
||||
{
|
||||
<p>How much did you add for this item?</p>
|
||||
<p>@L["RegistryPublic.HowMuchAdded"]</p>
|
||||
<InputNumber class="form-control" @bind-Value="ContributionAmount" @bind-Value:after="OnContributionAmountChanged" />
|
||||
<p class="mt-2">Message: @ContributionMessage</p>
|
||||
<p class="mt-2">@L["Common.Message"]: @ContributionMessage</p>
|
||||
|
||||
@if (SelectedPaymentMethod == ContributionPaymentMethodType.AmountSpecificQrCode && ContributionAmount > 0 && string.IsNullOrWhiteSpace(SelectedPaymentQrCodeUrl))
|
||||
{
|
||||
<p class="text-muted mt-2 mb-0">No QR code configured for this exact amount.</p>
|
||||
<p class="text-muted mt-2 mb-0">@L["RegistryPublic.NoQrForAmount"]</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SelectedPaymentQrCodeUrl))
|
||||
@@ -334,15 +349,15 @@ else
|
||||
<div class="mt-3 text-center">
|
||||
<img src="@BuildQrImageUrl(SelectedPaymentQrCodeUrl)" alt="Payment QR code" class="img-fluid" style="max-height: 240px;" />
|
||||
<div class="mt-2">
|
||||
<a href="@SelectedPaymentQrCodeUrl" target="_blank" rel="noopener noreferrer">Open payment link</a>
|
||||
<a href="@SelectedPaymentQrCodeUrl" target="_blank" rel="noopener noreferrer">@L["RegistryPublic.OpenPaymentLink"]</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success" @onclick="ConfirmContributionAsync">Confirm</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="BackContributionWizard">Back</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionPrompt">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="ConfirmContributionAsync">@L["RegistryPublic.Confirm"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="BackContributionWizard">@L["Common.Back"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionPrompt">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -353,14 +368,14 @@ else
|
||||
{
|
||||
<div class="prompt-overlay">
|
||||
<div class="prompt-card">
|
||||
<h3>Manage purchase</h3>
|
||||
<h3>@L["RegistryPublic.ManagePurchase"]</h3>
|
||||
|
||||
@if (Registry?.IsAdmin == true)
|
||||
{
|
||||
<label class="form-label">User</label>
|
||||
<InputText class="form-control mb-2" @bind-Value="UserFilterText" placeholder="Search user" />
|
||||
<label class="form-label">@L["RegistryPublic.User"]</label>
|
||||
<InputText class="form-control mb-2" @bind-Value="UserFilterText" placeholder="@L["Common.SearchUser"]" />
|
||||
<select class="form-select mb-3" value="@SelectedPurchaseUserId" @onchange="OnPurchaseManagedUserChanged">
|
||||
<option value="">Select user</option>
|
||||
<option value="">@L["Common.SelectUser"]</option>
|
||||
@foreach (var user in FilteredSelectableUsers)
|
||||
{
|
||||
<option value="@user.UserId">@user.DisplayName</option>
|
||||
@@ -368,13 +383,13 @@ else
|
||||
</select>
|
||||
}
|
||||
|
||||
<label class="form-label">Quantity</label>
|
||||
<label class="form-label">@L["Common.Quantity"]</label>
|
||||
<InputNumber class="form-control" @bind-Value="ManagedPurchaseQuantity" />
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success" @onclick="SavePurchaseManagementAsync">Save</button>
|
||||
<button class="btn btn-outline-danger" @onclick="RemoveManagedPurchaseAsync">Remove</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchaseManagementPrompt">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="SavePurchaseManagementAsync">@L["Common.Save"]</button>
|
||||
<button class="btn btn-outline-danger" @onclick="RemoveManagedPurchaseAsync">@L["Common.Remove"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="ClosePurchaseManagementPrompt">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,14 +399,14 @@ else
|
||||
{
|
||||
<div class="prompt-overlay">
|
||||
<div class="prompt-card">
|
||||
<h3>Manage participation</h3>
|
||||
<h3>@L["RegistryPublic.ManageParticipation"]</h3>
|
||||
|
||||
@if (Registry?.IsAdmin == true)
|
||||
{
|
||||
<label class="form-label">User</label>
|
||||
<InputText class="form-control mb-2" @bind-Value="UserFilterText" placeholder="Search user" />
|
||||
<label class="form-label">@L["RegistryPublic.User"]</label>
|
||||
<InputText class="form-control mb-2" @bind-Value="UserFilterText" placeholder="@L["Common.SearchUser"]" />
|
||||
<select class="form-select mb-3" value="@SelectedContributionUserId" @onchange="OnContributionManagedUserChanged">
|
||||
<option value="">Select user</option>
|
||||
<option value="">@L["Common.SelectUser"]</option>
|
||||
@foreach (var user in FilteredSelectableUsers)
|
||||
{
|
||||
<option value="@user.UserId">@user.DisplayName</option>
|
||||
@@ -399,16 +414,16 @@ else
|
||||
</select>
|
||||
}
|
||||
|
||||
<label class="form-label">Amount</label>
|
||||
<label class="form-label">@L["Common.Amount"]</label>
|
||||
<InputNumber class="form-control" @bind-Value="ManagedContributionAmount" />
|
||||
|
||||
<label class="form-label mt-2">Message</label>
|
||||
<label class="form-label mt-2">@L["Common.Message"]</label>
|
||||
<InputText class="form-control" @bind-Value="ManagedContributionMessage" />
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success" @onclick="SaveContributionManagementAsync">Save</button>
|
||||
<button class="btn btn-outline-danger" @onclick="RemoveManagedContributionAsync">Remove</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionManagementPrompt">Cancel</button>
|
||||
<button class="btn btn-success" @onclick="SaveContributionManagementAsync">@L["Common.Save"]</button>
|
||||
<button class="btn btn-outline-danger" @onclick="RemoveManagedContributionAsync">@L["Common.Remove"]</button>
|
||||
<button class="btn btn-outline-secondary" @onclick="CloseContributionManagementPrompt">@L["Common.Cancel"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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));
|
||||
@@ -168,3 +182,16 @@
|
||||
.category-toggle span.bi {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Render Quill text-size classes in public header content */
|
||||
.header-content ::deep .ql-size-small {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.header-content ::deep .ql-size-large {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.header-content ::deep .ql-size-huge {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using BirthList.Web
|
||||
@using BirthList.Web.Components
|
||||
|
||||
@inject IStringLocalizer<SharedResources> L
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace BirthList.Web.Configuration;
|
||||
|
||||
internal sealed class AmazonMetadataOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Amazon PA API access key. When set together with <see cref="SecretKey"/> and <see cref="AssociateTag"/>,
|
||||
/// the PA API is used as the primary metadata source for Amazon URLs.
|
||||
/// </summary>
|
||||
public string? AccessKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amazon PA API secret key.
|
||||
/// </summary>
|
||||
public string? SecretKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amazon Associates tag (e.g. "yourstore-21").
|
||||
/// </summary>
|
||||
public string? AssociateTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amazon PA API marketplace host, e.g. "webservices.amazon.com" or "webservices.amazon.com.be".
|
||||
/// Defaults to "webservices.amazon.com".
|
||||
/// </summary>
|
||||
public string PaApiHost { get; set; } = "webservices.amazon.com";
|
||||
|
||||
/// <summary>
|
||||
/// RapidAPI key for the "Real-Time Amazon Data" API.
|
||||
/// When set, this is used as a fallback when the direct scrape returns no metadata.
|
||||
/// </summary>
|
||||
public string? RapidApiKey { get; set; }
|
||||
|
||||
/// <summary>Returns true when PA API credentials are fully configured.</summary>
|
||||
public bool IsPaApiConfigured =>
|
||||
!string.IsNullOrWhiteSpace(AccessKey) &&
|
||||
!string.IsNullOrWhiteSpace(SecretKey) &&
|
||||
!string.IsNullOrWhiteSpace(AssociateTag);
|
||||
|
||||
/// <summary>Returns true when the RapidAPI fallback is configured.</summary>
|
||||
public bool IsRapidApiConfigured => !string.IsNullOrWhiteSpace(RapidApiKey);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BirthList.Web.Data;
|
||||
@@ -8,5 +9,8 @@ public class ApplicationUser : IdentityUser
|
||||
public string? FirstName { get; set; }
|
||||
public string? LastName { get; set; }
|
||||
public string? Address { get; set; }
|
||||
|
||||
[MaxLength(10)]
|
||||
public string? PreferredCulture { get; set; }
|
||||
}
|
||||
|
||||
|
||||
Generated
+292
@@ -0,0 +1,292 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using BirthList.Web.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BirthList.Web.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20260519000100_AddPreferredCultureToApplicationUser")]
|
||||
partial class AddPreferredCultureToApplicationUser
|
||||
{
|
||||
/// <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.Web.Data.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("PreferredCulture")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex")
|
||||
.HasFilter("[NormalizedUserName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex")
|
||||
.HasFilter("[NormalizedName] IS NOT NULL");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BirthList.Web.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BirthList.Web.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("BirthList.Web.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BirthList.Web.Data.ApplicationUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BirthList.Web.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPreferredCultureToApplicationUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PreferredCulture",
|
||||
table: "AspNetUsers",
|
||||
type: "nvarchar(10)",
|
||||
maxLength: 10,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PreferredCulture",
|
||||
table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,10 @@ namespace BirthList.Web.Migrations
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("PreferredCulture")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using BirthList.Web.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
|
||||
namespace BirthList.Web.Features.Localization;
|
||||
|
||||
internal sealed class LocalizationService(UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
private static readonly HashSet<string> SupportedCultureNames =
|
||||
[
|
||||
"nl-BE",
|
||||
"en",
|
||||
"fr-FR",
|
||||
"qps-Ploc"
|
||||
];
|
||||
|
||||
public static IReadOnlyList<string> GetSupportedCultures()
|
||||
{
|
||||
return SupportedCultureNames.OrderBy(x => x).ToList();
|
||||
}
|
||||
|
||||
public bool IsSupportedCulture(string? culture)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(culture) && SupportedCultureNames.Contains(culture);
|
||||
}
|
||||
|
||||
public async Task SetPreferredCultureAsync(ApplicationUser user, string culture, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
|
||||
if (!IsSupportedCulture(culture))
|
||||
{
|
||||
throw new ArgumentException("Unsupported culture.", nameof(culture));
|
||||
}
|
||||
|
||||
if (string.Equals(user.PreferredCulture, culture, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
user.PreferredCulture = culture;
|
||||
var result = await userManager.UpdateAsync(user).ConfigureAwait(false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var error = result.Errors.FirstOrDefault()?.Description ?? "Could not persist preferred culture.";
|
||||
throw new InvalidOperationException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public static string BuildCultureCookieValue(string culture)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(culture);
|
||||
return CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Json;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using BirthList.Web.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BirthList.Web.Features.Registries;
|
||||
|
||||
/// <summary>
|
||||
/// Calls the Amazon Product Advertising API 5.0 to retrieve product metadata.
|
||||
/// Implements AWS Signature Version 4 signing without external SDK dependencies.
|
||||
/// </summary>
|
||||
internal sealed class AmazonPaApiClient(IHttpClientFactory httpClientFactory, IOptions<AmazonMetadataOptions> options)
|
||||
{
|
||||
private const string Service = "ProductAdvertisingAPI";
|
||||
private const string Region = "us-east-1";
|
||||
private const string Operation = "GetItems";
|
||||
|
||||
private readonly AmazonMetadataOptions _options = options.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches product metadata for the given ASIN. Returns null when the item is not found or credentials are not configured.
|
||||
/// </summary>
|
||||
public async Task<UrlMetadataResult?> GetItemAsync(string asin, string normalizedUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_options.IsPaApiConfigured)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = BuildPayload(asin);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||
var payloadHash = ComputeSha256Hex(payloadBytes);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var dateStamp = now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
|
||||
var amzDate = now.ToString("yyyyMMddTHHmmssZ", CultureInfo.InvariantCulture);
|
||||
var host = _options.PaApiHost;
|
||||
var path = "/paapi5/getitems";
|
||||
|
||||
var headers = new SortedDictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["content-encoding"] = "amz-1.0",
|
||||
["content-type"] = "application/json; charset=utf-8",
|
||||
["host"] = host,
|
||||
["x-amz-date"] = amzDate,
|
||||
["x-amz-target"] = $"com.amazon.paapi5.v1.ProductAdvertisingAPIv1.{Operation}"
|
||||
};
|
||||
|
||||
var signedHeaderNames = string.Join(";", headers.Keys);
|
||||
var canonicalHeaders = string.Concat(headers.Select(h => $"{h.Key}:{h.Value}\n"));
|
||||
|
||||
var canonicalRequest = string.Join("\n",
|
||||
"POST",
|
||||
path,
|
||||
string.Empty,
|
||||
canonicalHeaders,
|
||||
signedHeaderNames,
|
||||
payloadHash);
|
||||
|
||||
var credentialScope = $"{dateStamp}/{Region}/{Service}/aws4_request";
|
||||
var stringToSign = string.Join("\n",
|
||||
"AWS4-HMAC-SHA256",
|
||||
amzDate,
|
||||
credentialScope,
|
||||
ComputeSha256Hex(Encoding.UTF8.GetBytes(canonicalRequest)));
|
||||
|
||||
var signingKey = GetSigningKey(_options.SecretKey!, dateStamp);
|
||||
var signature = ComputeHmacHex(signingKey, stringToSign);
|
||||
|
||||
var authorization =
|
||||
$"AWS4-HMAC-SHA256 Credential={_options.AccessKey}/{credentialScope}, " +
|
||||
$"SignedHeaders={signedHeaderNames}, Signature={signature}";
|
||||
|
||||
var client = httpClientFactory.CreateClient("AmazonPaApi");
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, $"https://{host}{path}");
|
||||
request.Content = new ByteArrayContent(payloadBytes);
|
||||
|
||||
foreach (var (key, value) in headers)
|
||||
{
|
||||
if (key is "host" or "content-type")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
request.Headers.TryAddWithoutValidation(key, value);
|
||||
}
|
||||
|
||||
request.Content.Headers.TryAddWithoutValidation("Content-Type", "application/json; charset=utf-8");
|
||||
request.Content.Headers.TryAddWithoutValidation("Content-Encoding", "amz-1.0");
|
||||
request.Headers.TryAddWithoutValidation("Authorization", authorization);
|
||||
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var paResponse = await response.Content.ReadFromJsonAsync<PaApiResponse>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
var item = paResponse?.ItemsResult?.Items?.FirstOrDefault();
|
||||
if (item is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var title = item.ItemInfo?.Title?.DisplayValue;
|
||||
var imageUrl = item.Images?.Primary?.Large?.Url ?? item.Images?.Primary?.Medium?.Url;
|
||||
var priceAmount = item.Offers?.Listings?.FirstOrDefault()?.Price?.Amount;
|
||||
var currency = item.Offers?.Listings?.FirstOrDefault()?.Price?.Currency;
|
||||
|
||||
return new UrlMetadataResult
|
||||
{
|
||||
NormalizedUrl = normalizedUrl,
|
||||
Title = title,
|
||||
Description = null,
|
||||
ImageUrl = imageUrl,
|
||||
PriceAmount = priceAmount,
|
||||
CurrencyCode = string.IsNullOrWhiteSpace(currency) ? null : currency.ToUpperInvariant()
|
||||
};
|
||||
}
|
||||
|
||||
private string BuildPayload(string asin)
|
||||
{
|
||||
var doc = new
|
||||
{
|
||||
ItemIds = new[] { asin },
|
||||
Resources = new[]
|
||||
{
|
||||
"ItemInfo.Title",
|
||||
"Images.Primary.Large",
|
||||
"Images.Primary.Medium",
|
||||
"Offers.Listings.Price"
|
||||
},
|
||||
PartnerTag = _options.AssociateTag,
|
||||
PartnerType = "Associates",
|
||||
Marketplace = $"www.{_options.PaApiHost.Replace("webservices.", string.Empty, StringComparison.OrdinalIgnoreCase)}"
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(doc);
|
||||
}
|
||||
|
||||
private static byte[] GetSigningKey(string secretKey, string dateStamp)
|
||||
{
|
||||
var kDate = ComputeHmac(Encoding.UTF8.GetBytes($"AWS4{secretKey}"), dateStamp);
|
||||
var kRegion = ComputeHmac(kDate, Region);
|
||||
var kService = ComputeHmac(kRegion, Service);
|
||||
return ComputeHmac(kService, "aws4_request");
|
||||
}
|
||||
|
||||
private static byte[] ComputeHmac(byte[] key, string data) =>
|
||||
HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(data));
|
||||
|
||||
private static string ComputeHmacHex(byte[] key, string data) =>
|
||||
Convert.ToHexString(ComputeHmac(key, data)).ToLowerInvariant();
|
||||
|
||||
private static string ComputeSha256Hex(byte[] data) =>
|
||||
Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();
|
||||
|
||||
// Minimal deserialization models for PA API response
|
||||
|
||||
private sealed class PaApiResponse
|
||||
{
|
||||
[JsonPropertyName("ItemsResult")]
|
||||
public PaItemsResult? ItemsResult { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaItemsResult
|
||||
{
|
||||
[JsonPropertyName("Items")]
|
||||
public List<PaItem>? Items { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaItem
|
||||
{
|
||||
[JsonPropertyName("ItemInfo")]
|
||||
public PaItemInfo? ItemInfo { get; init; }
|
||||
|
||||
[JsonPropertyName("Images")]
|
||||
public PaImages? Images { get; init; }
|
||||
|
||||
[JsonPropertyName("Offers")]
|
||||
public PaOffers? Offers { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaItemInfo
|
||||
{
|
||||
[JsonPropertyName("Title")]
|
||||
public PaDisplayValue? Title { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaDisplayValue
|
||||
{
|
||||
[JsonPropertyName("DisplayValue")]
|
||||
public string? DisplayValue { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaImages
|
||||
{
|
||||
[JsonPropertyName("Primary")]
|
||||
public PaImageSet? Primary { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaImageSet
|
||||
{
|
||||
[JsonPropertyName("Large")]
|
||||
public PaImageVariant? Large { get; init; }
|
||||
|
||||
[JsonPropertyName("Medium")]
|
||||
public PaImageVariant? Medium { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaImageVariant
|
||||
{
|
||||
[JsonPropertyName("URL")]
|
||||
public string? Url { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaOffers
|
||||
{
|
||||
[JsonPropertyName("Listings")]
|
||||
public List<PaListing>? Listings { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaListing
|
||||
{
|
||||
[JsonPropertyName("Price")]
|
||||
public PaPrice? Price { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PaPrice
|
||||
{
|
||||
[JsonPropertyName("Amount")]
|
||||
public decimal? Amount { get; init; }
|
||||
|
||||
[JsonPropertyName("Currency")]
|
||||
public string? Currency { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using BirthList.Web.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BirthList.Web.Features.Registries;
|
||||
|
||||
/// <summary>
|
||||
/// Calls the "Real-Time Amazon Data" API on RapidAPI as a metadata fallback.
|
||||
/// https://rapidapi.com/letscrape-6bRBa3QguO5/api/real-time-amazon-data
|
||||
/// </summary>
|
||||
internal sealed class RapidApiMetadataClient(IHttpClientFactory httpClientFactory, IOptions<AmazonMetadataOptions> options)
|
||||
{
|
||||
private const string Host = "real-time-amazon-data.p.rapidapi.com";
|
||||
private readonly AmazonMetadataOptions _options = options.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches product metadata for the given ASIN and Amazon country code (e.g. "BE", "US", "DE").
|
||||
/// Returns null when the API key is not configured or the item is not found.
|
||||
/// </summary>
|
||||
public async Task<UrlMetadataResult?> GetItemAsync(string asin, string countryCode, string normalizedUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_options.IsRapidApiConfigured)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient("RapidApi");
|
||||
using var request = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
$"https://{Host}/product-details?asin={Uri.EscapeDataString(asin)}&country={Uri.EscapeDataString(countryCode)}");
|
||||
|
||||
request.Headers.TryAddWithoutValidation("x-rapidapi-host", Host);
|
||||
request.Headers.TryAddWithoutValidation("x-rapidapi-key", _options.RapidApiKey);
|
||||
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<RapidApiProductResponse>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (result?.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = result.Data;
|
||||
|
||||
var imageUrl = data.ProductMainImageUrl
|
||||
?? data.ProductPhotos?.FirstOrDefault();
|
||||
|
||||
decimal? price = null;
|
||||
string? currency = null;
|
||||
var priceEntry = data.ProductPrice ?? data.ProductOriginalPrice;
|
||||
if (!string.IsNullOrWhiteSpace(priceEntry))
|
||||
{
|
||||
(price, currency) = ParseRapidApiPrice(priceEntry);
|
||||
}
|
||||
|
||||
return new UrlMetadataResult
|
||||
{
|
||||
NormalizedUrl = normalizedUrl,
|
||||
Title = data.ProductTitle,
|
||||
Description = data.ProductDescription,
|
||||
ImageUrl = imageUrl,
|
||||
PriceAmount = price,
|
||||
CurrencyCode = currency
|
||||
};
|
||||
}
|
||||
|
||||
private static (decimal? amount, string? currency) ParseRapidApiPrice(string raw)
|
||||
{
|
||||
// RapidAPI returns prices like "€29,99" or "$19.99" or "29.99 EUR"
|
||||
string? currencyCode = null;
|
||||
|
||||
if (raw.Contains('€')) currencyCode = "EUR";
|
||||
else if (raw.Contains('$')) currencyCode = "USD";
|
||||
else if (raw.Contains('£')) currencyCode = "GBP";
|
||||
else
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(raw, @"\b([A-Z]{3})\b");
|
||||
if (match.Success) currencyCode = match.Value;
|
||||
}
|
||||
|
||||
var cleaned = System.Text.RegularExpressions.Regex.Replace(raw, "[^0-9.,]", string.Empty);
|
||||
cleaned = cleaned.Replace(",", ".");
|
||||
|
||||
if (decimal.TryParse(cleaned, System.Globalization.NumberStyles.Number,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var amount))
|
||||
{
|
||||
return (amount, currencyCode);
|
||||
}
|
||||
|
||||
return (null, currencyCode);
|
||||
}
|
||||
|
||||
// Minimal deserialization models
|
||||
|
||||
private sealed class RapidApiProductResponse
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public RapidApiProductData? Data { get; init; }
|
||||
}
|
||||
|
||||
private sealed class RapidApiProductData
|
||||
{
|
||||
[JsonPropertyName("product_title")]
|
||||
public string? ProductTitle { get; init; }
|
||||
|
||||
[JsonPropertyName("product_description")]
|
||||
public string? ProductDescription { get; init; }
|
||||
|
||||
[JsonPropertyName("product_main_image_url")]
|
||||
public string? ProductMainImageUrl { get; init; }
|
||||
|
||||
[JsonPropertyName("product_photos")]
|
||||
public List<string>? ProductPhotos { get; init; }
|
||||
|
||||
[JsonPropertyName("product_price")]
|
||||
public string? ProductPrice { get; init; }
|
||||
|
||||
[JsonPropertyName("product_original_price")]
|
||||
public string? ProductOriginalPrice { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,14 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace BirthList.Web.Features.Registries;
|
||||
|
||||
internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFactory)
|
||||
internal sealed class RegistryMetadataService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
AmazonPaApiClient paApiClient,
|
||||
RapidApiMetadataClient rapidApiClient)
|
||||
{
|
||||
private static readonly Regex MetaTagRegex = new("<meta\\b[^>]*>", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||
private static readonly Regex AttributeRegex = new("(name|property|content)\\s*=\\s*['\"]([^'\"]*)['\"]", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||
private static readonly Regex AsinRegex = new(@"/dp/(?<asin>[A-Z0-9]{10})(?:/|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(1));
|
||||
|
||||
public async Task<UrlMetadataResult?> FetchAsync(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -16,22 +20,83 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
|
||||
}
|
||||
|
||||
var normalizedUri = NormalizeProductUri(sourceUri);
|
||||
var isAmazon = normalizedUri.Host.Contains("amazon", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var client = httpClientFactory.CreateClient("RegistryMetadata");
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, normalizedUri);
|
||||
request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (compatible; BirthListBot/1.0)");
|
||||
request.Headers.AcceptLanguage.ParseAdd("en-US,en;q=0.9");
|
||||
|
||||
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
// Strategy 1: PA API (Amazon only, when credentials are configured)
|
||||
if (isAmazon)
|
||||
{
|
||||
return null;
|
||||
var asin = ExtractAsin(normalizedUri);
|
||||
if (!string.IsNullOrWhiteSpace(asin))
|
||||
{
|
||||
var paResult = await paApiClient.GetItemAsync(asin, normalizedUri.AbsoluteUri, cancellationToken).ConfigureAwait(false);
|
||||
if (paResult is not null && HasUsefulMetadata(paResult))
|
||||
{
|
||||
return paResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Direct HTML scrape
|
||||
var scrapeResult = await ScrapeAsync(normalizedUri, cancellationToken).ConfigureAwait(false);
|
||||
if (scrapeResult is not null && HasUsefulMetadata(scrapeResult))
|
||||
{
|
||||
return scrapeResult;
|
||||
}
|
||||
|
||||
// Strategy 3: RapidAPI fallback (Amazon only)
|
||||
if (isAmazon)
|
||||
{
|
||||
var asin = ExtractAsin(normalizedUri);
|
||||
if (!string.IsNullOrWhiteSpace(asin))
|
||||
{
|
||||
var countryCode = ExtractAmazonCountryCode(normalizedUri);
|
||||
var rapidResult = await rapidApiClient.GetItemAsync(asin, countryCode, normalizedUri.AbsoluteUri, cancellationToken).ConfigureAwait(false);
|
||||
if (rapidResult is not null)
|
||||
{
|
||||
return rapidResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return whatever the scrape got (may be partial or null) rather than hard-failing
|
||||
return scrapeResult;
|
||||
}
|
||||
|
||||
private async Task<UrlMetadataResult?> ScrapeAsync(Uri normalizedUri, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = httpClientFactory.CreateClient("RegistryMetadata");
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, normalizedUri);
|
||||
request.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36");
|
||||
request.Headers.TryAddWithoutValidation("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
|
||||
request.Headers.TryAddWithoutValidation("Accept-Encoding", "gzip, deflate, br");
|
||||
request.Headers.AcceptLanguage.ParseAdd("en-US,en;q=0.9");
|
||||
request.Headers.TryAddWithoutValidation("Upgrade-Insecure-Requests", "1");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-Dest", "document");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-Mode", "navigate");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "none");
|
||||
request.Headers.TryAddWithoutValidation("Sec-Fetch-User", "?1");
|
||||
request.Headers.TryAddWithoutValidation("Cache-Control", "max-age=0");
|
||||
|
||||
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string? html = null;
|
||||
try
|
||||
{
|
||||
html = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// Body unreadable; fall through with null html.
|
||||
}
|
||||
|
||||
var html = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
{
|
||||
return null;
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Request failed with status {(int)response.StatusCode} {response.ReasonPhrase} and no response body.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Metadata response content was empty.");
|
||||
}
|
||||
|
||||
var meta = ParseMeta(html);
|
||||
@@ -114,6 +179,15 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
|
||||
}
|
||||
}
|
||||
|
||||
var hasAnyMetadata = !string.IsNullOrWhiteSpace(title)
|
||||
|| !string.IsNullOrWhiteSpace(image)
|
||||
|| price.HasValue;
|
||||
|
||||
if (!hasAnyMetadata && !response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"Request failed with status {(int)response.StatusCode} {response.ReasonPhrase} and no metadata could be parsed from the response.");
|
||||
}
|
||||
|
||||
return new UrlMetadataResult
|
||||
{
|
||||
NormalizedUrl = normalizedUri.AbsoluteUri,
|
||||
@@ -125,6 +199,34 @@ internal sealed class RegistryMetadataService(IHttpClientFactory httpClientFacto
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasUsefulMetadata(UrlMetadataResult result) =>
|
||||
!string.IsNullOrWhiteSpace(result.Title) ||
|
||||
!string.IsNullOrWhiteSpace(result.ImageUrl) ||
|
||||
result.PriceAmount.HasValue;
|
||||
|
||||
private static string? ExtractAsin(Uri uri)
|
||||
{
|
||||
var match = AsinRegex.Match(uri.AbsolutePath);
|
||||
return match.Success ? match.Groups["asin"].Value.ToUpperInvariant() : null;
|
||||
}
|
||||
|
||||
private static string ExtractAmazonCountryCode(Uri uri)
|
||||
{
|
||||
// amazon.com.be -> BE, amazon.de -> DE, amazon.co.uk -> GB, amazon.com -> US
|
||||
var host = uri.Host.ToLowerInvariant();
|
||||
if (host.EndsWith(".com.be", StringComparison.Ordinal)) return "BE";
|
||||
if (host.EndsWith(".de", StringComparison.Ordinal)) return "DE";
|
||||
if (host.EndsWith(".fr", StringComparison.Ordinal)) return "FR";
|
||||
if (host.EndsWith(".nl", StringComparison.Ordinal)) return "NL";
|
||||
if (host.EndsWith(".co.uk", StringComparison.Ordinal)) return "GB";
|
||||
if (host.EndsWith(".es", StringComparison.Ordinal)) return "ES";
|
||||
if (host.EndsWith(".it", StringComparison.Ordinal)) return "IT";
|
||||
if (host.EndsWith(".ca", StringComparison.Ordinal)) return "CA";
|
||||
if (host.EndsWith(".com.au", StringComparison.Ordinal)) return "AU";
|
||||
if (host.EndsWith(".co.jp", StringComparison.Ordinal)) return "JP";
|
||||
return "US";
|
||||
}
|
||||
|
||||
private static bool IsGenericAmazonImage(string image)
|
||||
{
|
||||
return image.Contains("amazon.png", StringComparison.OrdinalIgnoreCase)
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed class RegistryItemEditModel
|
||||
public string? ProductUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public decimal? PriceAmount { get; set; }
|
||||
public string CurrencyCode { get; set; } = "EUR";
|
||||
public string CurrencyCode { get; set; } = "€";
|
||||
public int DesiredQuantity { get; set; } = 1;
|
||||
public bool ParticipationAllowed { get; set; }
|
||||
public decimal? ParticipationTargetAmount { get; set; }
|
||||
@@ -53,16 +53,21 @@ 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? HeroImagePath { get; set; }
|
||||
public string? HeaderContentHtml { get; set; }
|
||||
public string? ShippingAddress { get; set; }
|
||||
public string CurrencyCode { get; set; } = "EUR";
|
||||
public string CurrencyCode { get; set; } = "€";
|
||||
public string ThemeKey { get; set; } = "default";
|
||||
public string? BankAccountIban { get; set; }
|
||||
public string? BankAccountBic { get; set; }
|
||||
public string? BankAccountDisplayName { get; set; }
|
||||
public bool ShowBankAccountName { get; set; }
|
||||
public bool HideHeaderName { get; set; }
|
||||
public string? ContributionQrCodeUrl { get; set; }
|
||||
public List<ContributionAmountQrCodeModel> ContributionAmountQrCodes { get; set; } = [];
|
||||
public string PublicLinkCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class RegistrySummaryViewModel
|
||||
@@ -80,15 +85,19 @@ 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? HeroImagePath { get; init; }
|
||||
public string? HeaderContentHtml { get; init; }
|
||||
public string? ShippingAddress { get; init; }
|
||||
public string CurrencyCode { get; init; } = "EUR";
|
||||
public string CurrencyCode { get; init; } = "€";
|
||||
public string ThemeKey { get; init; } = "default";
|
||||
public RegistryType RegistryType { get; init; }
|
||||
public string? BankAccountIban { get; init; }
|
||||
public string? BankAccountBic { get; init; }
|
||||
public string? BankAccountDisplayName { get; init; }
|
||||
public bool ShowBankAccountName { get; init; }
|
||||
public bool HideHeaderName { get; init; }
|
||||
public string? ContributionQrCodeUrl { get; init; }
|
||||
public IReadOnlyList<ContributionAmountQrCodeModel> ContributionAmountQrCodes { get; init; } = [];
|
||||
public string? CurrentUserId { get; init; }
|
||||
@@ -117,7 +126,7 @@ public sealed class RegistryPublicItemViewModel
|
||||
public string? ProductUrl { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public decimal? PriceAmount { get; init; }
|
||||
public string CurrencyCode { get; init; } = "EUR";
|
||||
public string CurrencyCode { get; init; } = "€";
|
||||
public int DesiredQuantity { get; init; }
|
||||
public int PurchasedQuantity { get; init; }
|
||||
public bool ParticipationAllowed { get; init; }
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace BirthList.Web.Features.Registries;
|
||||
internal sealed class RegistryService(RegistryDbContext registryDbContext, ApplicationDbContext applicationDbContext)
|
||||
{
|
||||
private const string DefaultCategoryName = "General";
|
||||
private const string DefaultCurrencySymbol = "€";
|
||||
|
||||
public async Task<Guid> CreateRegistryAsync(string userId, RegistryCreateModel model, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -31,7 +32,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
||||
ThemeKey = string.IsNullOrWhiteSpace(model.ThemeKey) ? "default" : model.ThemeKey.Trim(),
|
||||
PublicLinkCode = publicCode,
|
||||
CreatedAtUtc = DateTimeOffset.UtcNow,
|
||||
CurrencyCode = "EUR"
|
||||
CurrencyCode = DefaultCurrencySymbol
|
||||
};
|
||||
|
||||
registryDbContext.Registries.Add(registry);
|
||||
@@ -298,6 +299,9 @@ 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,
|
||||
HeroImagePath = registry.HeroImagePath,
|
||||
HeaderContentHtml = registry.HeaderContentHtml,
|
||||
ShippingAddress = registry.ShippingAddress,
|
||||
CurrencyCode = registry.CurrencyCode,
|
||||
@@ -307,6 +311,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
||||
BankAccountBic = settings?.BankAccountBic,
|
||||
BankAccountDisplayName = settings?.BankAccountDisplayName,
|
||||
ShowBankAccountName = settings?.ShowBankAccountName ?? false,
|
||||
HideHeaderName = settings?.HideHeaderName ?? false,
|
||||
ContributionQrCodeUrl = settings?.ContributionQrCodeUrl,
|
||||
ContributionAmountQrCodes = ParseContributionAmountQrCodes(settings?.ContributionAmountQrCodesJson),
|
||||
CurrentUserId = userId,
|
||||
@@ -335,6 +340,9 @@ 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,
|
||||
HeroImagePath = registry.HeroImagePath,
|
||||
HeaderContentHtml = registry.HeaderContentHtml,
|
||||
ShippingAddress = registry.ShippingAddress,
|
||||
CurrencyCode = registry.CurrencyCode,
|
||||
@@ -343,8 +351,10 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
||||
BankAccountBic = settings?.BankAccountBic,
|
||||
BankAccountDisplayName = settings?.BankAccountDisplayName,
|
||||
ShowBankAccountName = settings?.ShowBankAccountName ?? false,
|
||||
HideHeaderName = settings?.HideHeaderName ?? false,
|
||||
ContributionQrCodeUrl = settings?.ContributionQrCodeUrl,
|
||||
ContributionAmountQrCodes = ParseContributionAmountQrCodes(settings?.ContributionAmountQrCodesJson).ToList()
|
||||
ContributionAmountQrCodes = ParseContributionAmountQrCodes(settings?.ContributionAmountQrCodesJson).ToList(),
|
||||
PublicLinkCode = registry.PublicLinkCode
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,77 +381,43 @@ 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.HeroImagePath))
|
||||
{
|
||||
registry.HeroImagePath = model.HeroImagePath.Trim();
|
||||
registry.HeroImageData = null;
|
||||
registry.HeroImageContentType = null;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(model.HeroImageBase64))
|
||||
{
|
||||
registry.HeroImageData = Convert.FromBase64String(model.HeroImageBase64);
|
||||
registry.HeroImageContentType = model.HeroImageContentType;
|
||||
registry.HeroImagePath = null;
|
||||
}
|
||||
else if ((string.IsNullOrWhiteSpace(model.HeroImageBase64) && model.HeroImageBase64 == string.Empty)
|
||||
|| (string.IsNullOrWhiteSpace(model.HeroImagePath) && model.HeroImagePath == string.Empty))
|
||||
{
|
||||
registry.HeroImageData = null;
|
||||
registry.HeroImageContentType = null;
|
||||
registry.HeroImagePath = null;
|
||||
}
|
||||
|
||||
registry.HeaderContentHtml = string.IsNullOrWhiteSpace(model.HeaderContentHtml) ? null : model.HeaderContentHtml;
|
||||
registry.ShippingAddress = string.IsNullOrWhiteSpace(model.ShippingAddress) ? null : model.ShippingAddress.Trim();
|
||||
registry.CurrencyCode = string.IsNullOrWhiteSpace(model.CurrencyCode) ? "EUR" : model.CurrencyCode.Trim().ToUpperInvariant();
|
||||
registry.CurrencyCode = NormalizeCurrencySymbol(model.CurrencyCode);
|
||||
registry.ThemeKey = string.IsNullOrWhiteSpace(model.ThemeKey) ? "default" : model.ThemeKey.Trim();
|
||||
|
||||
settings.BankAccountIban = string.IsNullOrWhiteSpace(model.BankAccountIban) ? null : model.BankAccountIban.Trim();
|
||||
settings.BankAccountBic = string.IsNullOrWhiteSpace(model.BankAccountBic) ? null : model.BankAccountBic.Trim();
|
||||
settings.BankAccountDisplayName = string.IsNullOrWhiteSpace(model.BankAccountDisplayName) ? null : model.BankAccountDisplayName.Trim();
|
||||
settings.ShowBankAccountName = model.ShowBankAccountName;
|
||||
settings.HideHeaderName = model.HideHeaderName;
|
||||
settings.ContributionQrCodeUrl = string.IsNullOrWhiteSpace(model.ContributionQrCodeUrl) ? null : model.ContributionQrCodeUrl.Trim();
|
||||
settings.ContributionAmountQrCodesJson = SerializeContributionAmountQrCodes(model.ContributionAmountQrCodes);
|
||||
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ContributionAmountQrCodeModel> ParseContributionAmountQrCodes(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var items = JsonSerializer.Deserialize<List<ContributionAmountQrCodeModel>>(json);
|
||||
if (items is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return items
|
||||
.Where(x => x.Amount > 0 && !string.IsNullOrWhiteSpace(x.QrCodeUrl))
|
||||
.Select(x => new ContributionAmountQrCodeModel
|
||||
{
|
||||
Amount = x.Amount,
|
||||
QrCodeUrl = x.QrCodeUrl.Trim()
|
||||
})
|
||||
.OrderBy(x => x.Amount)
|
||||
.ToList();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SerializeContributionAmountQrCodes(IEnumerable<ContributionAmountQrCodeModel>? amountQrCodes)
|
||||
{
|
||||
if (amountQrCodes is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = amountQrCodes
|
||||
.Where(x => x.Amount > 0 && !string.IsNullOrWhiteSpace(x.QrCodeUrl))
|
||||
.Select(x => new ContributionAmountQrCodeModel
|
||||
{
|
||||
Amount = x.Amount,
|
||||
QrCodeUrl = x.QrCodeUrl.Trim()
|
||||
})
|
||||
.OrderBy(x => x.Amount)
|
||||
.ToList();
|
||||
|
||||
if (normalized.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(normalized);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RegistryItemEditModel>> GetRegistryItemsAsync(Guid registryId, CancellationToken cancellationToken)
|
||||
{
|
||||
var defaultCategory = await EnsureDefaultCategoryAsync(registryId, cancellationToken).ConfigureAwait(false);
|
||||
@@ -682,7 +658,7 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
||||
entity.ProductUrl = string.IsNullOrWhiteSpace(model.ProductUrl) ? null : model.ProductUrl.Trim();
|
||||
entity.Description = string.IsNullOrWhiteSpace(model.Description) ? null : model.Description.Trim();
|
||||
entity.PriceAmount = model.PriceAmount;
|
||||
entity.CurrencyCode = string.IsNullOrWhiteSpace(model.CurrencyCode) ? "EUR" : model.CurrencyCode.Trim().ToUpperInvariant();
|
||||
entity.CurrencyCode = NormalizeCurrencySymbol(model.CurrencyCode);
|
||||
entity.DesiredQuantity = model.DesiredQuantity < 1 ? 1 : model.DesiredQuantity;
|
||||
entity.ParticipationAllowed = model.ParticipationAllowed;
|
||||
entity.ParticipationTargetAmount = model.ParticipationAllowed ? model.ParticipationTargetAmount : null;
|
||||
@@ -1629,4 +1605,67 @@ internal sealed class RegistryService(RegistryDbContext registryDbContext, Appli
|
||||
await registryDbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ContributionAmountQrCodeModel> ParseContributionAmountQrCodes(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var items = JsonSerializer.Deserialize<List<ContributionAmountQrCodeModel>>(json);
|
||||
if (items is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return items
|
||||
.Where(x => x.Amount > 0 && !string.IsNullOrWhiteSpace(x.QrCodeUrl))
|
||||
.Select(x => new ContributionAmountQrCodeModel
|
||||
{
|
||||
Amount = x.Amount,
|
||||
QrCodeUrl = x.QrCodeUrl.Trim()
|
||||
})
|
||||
.OrderBy(x => x.Amount)
|
||||
.ToList();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SerializeContributionAmountQrCodes(IEnumerable<ContributionAmountQrCodeModel>? amountQrCodes)
|
||||
{
|
||||
if (amountQrCodes is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = amountQrCodes
|
||||
.Where(x => x.Amount > 0 && !string.IsNullOrWhiteSpace(x.QrCodeUrl))
|
||||
.Select(x => new ContributionAmountQrCodeModel
|
||||
{
|
||||
Amount = x.Amount,
|
||||
QrCodeUrl = x.QrCodeUrl.Trim()
|
||||
})
|
||||
.OrderBy(x => x.Amount)
|
||||
.ToList();
|
||||
|
||||
if (normalized.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(normalized);
|
||||
}
|
||||
|
||||
private static string NormalizeCurrencySymbol(string? currencySymbol)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(currencySymbol)
|
||||
? DefaultCurrencySymbol
|
||||
: currencySymbol.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using BirthList.Infrastructure.Persistence;
|
||||
using BirthList.Web.Authorization;
|
||||
using BirthList.Web.Components;
|
||||
using BirthList.Web.Components.Account;
|
||||
using BirthList.Web.Configuration;
|
||||
using BirthList.Web.Data;
|
||||
using BirthList.Web.Features.Localization;
|
||||
using BirthList.Web.Features.Registries;
|
||||
using BirthList.Web.Services;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
@@ -24,7 +28,16 @@ builder.Services.AddHttpClient("RegistryMetadata", client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
builder.Services.AddHttpClient("AmazonPaApi", client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
builder.Services.AddHttpClient("RapidApi", client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
});
|
||||
builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"));
|
||||
builder.Services.Configure<AmazonMetadataOptions>(builder.Configuration.GetSection("AmazonMetadata"));
|
||||
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
@@ -41,9 +54,12 @@ builder.Services.AddScoped<RegistryAuthorizationService>();
|
||||
builder.Services.AddScoped<OwnerBootstrapService>();
|
||||
builder.Services.AddScoped<RegistryService>();
|
||||
builder.Services.AddScoped<RegistryMetadataService>();
|
||||
builder.Services.AddScoped<AmazonPaApiClient>();
|
||||
builder.Services.AddScoped<RapidApiMetadataClient>();
|
||||
builder.Services.AddScoped<RegistryThemeService>();
|
||||
builder.Services.AddScoped<RegistryUserContext>();
|
||||
builder.Services.AddScoped<SmtpConfigurationStatusService>();
|
||||
builder.Services.AddScoped<LocalizationService>();
|
||||
|
||||
var googleClientId = builder.Configuration["Authentication:Google:ClientId"];
|
||||
var googleClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
|
||||
@@ -126,6 +142,35 @@ builder.Services.AddScoped<SmtpEmailSender>();
|
||||
builder.Services.AddScoped<IEmailSender<ApplicationUser>>(serviceProvider => serviceProvider.GetRequiredService<SmtpEmailSender>());
|
||||
builder.Services.AddScoped<ProfileCompletionService>();
|
||||
|
||||
builder.Services.AddLocalization(options =>
|
||||
{
|
||||
options.ResourcesPath = "Resources";
|
||||
});
|
||||
|
||||
var supportedCultures = new List<CultureInfo>
|
||||
{
|
||||
new("nl-BE"),
|
||||
new("en"),
|
||||
new("fr-FR")
|
||||
};
|
||||
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
supportedCultures.Add(new CultureInfo("qps-Ploc"));
|
||||
}
|
||||
|
||||
builder.Services.Configure<RequestLocalizationOptions>(options =>
|
||||
{
|
||||
options.DefaultRequestCulture = new RequestCulture("nl-BE");
|
||||
options.SupportedCultures = supportedCultures;
|
||||
options.SupportedUICultures = supportedCultures;
|
||||
options.RequestCultureProviders =
|
||||
[
|
||||
new CookieRequestCultureProvider(),
|
||||
new AcceptLanguageHeaderRequestCultureProvider()
|
||||
];
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
@@ -190,9 +235,50 @@ app.UseAuthorization();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapPost("/set-language", async (
|
||||
HttpContext context,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
LocalizationService localizationService,
|
||||
[FromForm] string culture,
|
||||
[FromForm] string? returnUrl) =>
|
||||
{
|
||||
if (!localizationService.IsSupportedCulture(culture))
|
||||
{
|
||||
return Results.BadRequest("Unsupported culture.");
|
||||
}
|
||||
|
||||
context.Response.Cookies.Append(
|
||||
CookieRequestCultureProvider.DefaultCookieName,
|
||||
LocalizationService.BuildCultureCookieValue(culture),
|
||||
new CookieOptions
|
||||
{
|
||||
IsEssential = true,
|
||||
Expires = DateTimeOffset.UtcNow.AddYears(1)
|
||||
});
|
||||
|
||||
if (context.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var user = await userManager.GetUserAsync(context.User).ConfigureAwait(false);
|
||||
if (user is not null)
|
||||
{
|
||||
await localizationService.SetPreferredCultureAsync(user, culture, context.RequestAborted).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var targetUrl = string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl;
|
||||
if (!Uri.TryCreate(targetUrl, UriKind.Relative, out _))
|
||||
{
|
||||
targetUrl = "/";
|
||||
}
|
||||
|
||||
return Results.LocalRedirect(targetUrl);
|
||||
});
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value></resheader>
|
||||
|
||||
<data name="Common.Yes" xml:space="preserve"><value>Oui</value></data>
|
||||
<data name="Common.No" xml:space="preserve"><value>Non</value></data>
|
||||
<data name="Common.Save" xml:space="preserve"><value>Enregistrer</value></data>
|
||||
<data name="Common.Cancel" xml:space="preserve"><value>Annuler</value></data>
|
||||
<data name="Common.Remove" xml:space="preserve"><value>Supprimer</value></data>
|
||||
<data name="Common.Edit" xml:space="preserve"><value>Modifier</value></data>
|
||||
<data name="Common.Back" xml:space="preserve"><value>Retour</value></data>
|
||||
<data name="Common.Next" xml:space="preserve"><value>Suivant</value></data>
|
||||
<data name="Common.Loading" xml:space="preserve"><value>Chargement...</value></data>
|
||||
<data name="Common.AccessDenied" xml:space="preserve"><value>Accès refusé.</value></data>
|
||||
<data name="Common.SelectUser" xml:space="preserve"><value>Sélectionner un utilisateur</value></data>
|
||||
<data name="Common.SearchUser" xml:space="preserve"><value>Rechercher un utilisateur</value></data>
|
||||
<data name="Common.Quantity" xml:space="preserve"><value>Quantité</value></data>
|
||||
<data name="Common.Amount" xml:space="preserve"><value>Montant</value></data>
|
||||
<data name="Common.Message" xml:space="preserve"><value>Message</value></data>
|
||||
|
||||
<data name="TopBar.Language" xml:space="preserve"><value>Langue</value></data>
|
||||
<data name="TopBar.Brand" xml:space="preserve"><value>Gift List</value></data>
|
||||
<data name="TopBar.ProfilePrompt" xml:space="preserve"><value>Veuillez compléter votre profil (prénom, nom et adresse).</value></data>
|
||||
<data name="TopBar.CompleteProfile" xml:space="preserve"><value>Compléter le profil</value></data>
|
||||
<data name="TopBar.AccountSettings" xml:space="preserve"><value>Paramètres du compte</value></data>
|
||||
<data name="TopBar.SignIn" xml:space="preserve"><value>Se connecter</value></data>
|
||||
<data name="TopBar.SignOut" xml:space="preserve"><value>Se déconnecter</value></data>
|
||||
|
||||
<data name="Home.PageTitle" xml:space="preserve"><value>Liste de naissance</value></data>
|
||||
<data name="Home.Welcome" xml:space="preserve"><value>Bienvenue sur Gift List</value></data>
|
||||
<data name="Home.ManagedRegistries" xml:space="preserve"><value>Listes que vous gérez</value></data>
|
||||
<data name="Home.CreateNew" xml:space="preserve"><value>Créer</value></data>
|
||||
<data name="Home.NoRegistries" xml:space="preserve"><value>Aucune liste pour le moment.</value></data>
|
||||
<data name="Home.View" xml:space="preserve"><value>Voir</value></data>
|
||||
<data name="Home.Manage" xml:space="preserve"><value>Gérer</value></data>
|
||||
<data name="Home.VisitedRegistries" xml:space="preserve"><value>Listes visitées</value></data>
|
||||
<data name="Home.NoVisitedRegistries" xml:space="preserve"><value>Aucune liste visitée pour le moment.</value></data>
|
||||
<data name="Home.CreateRegistryTitle" xml:space="preserve"><value>Créer une nouvelle liste</value></data>
|
||||
<data name="Home.Title" xml:space="preserve"><value>Titre</value></data>
|
||||
<data name="Home.Type" xml:space="preserve"><value>Type</value></data>
|
||||
<data name="Home.Theme" xml:space="preserve"><value>Thème</value></data>
|
||||
<data name="Home.Theme.Default" xml:space="preserve"><value>Par défaut</value></data>
|
||||
<data name="Home.Theme.Soft" xml:space="preserve"><value>Doux</value></data>
|
||||
<data name="Home.Theme.Modern" xml:space="preserve"><value>Moderne</value></data>
|
||||
<data name="Home.RegistryType.Birth" xml:space="preserve"><value>Naissance</value></data>
|
||||
<data name="Home.RegistryType.Wedding" xml:space="preserve"><value>Mariage</value></data>
|
||||
<data name="Home.RegistryType.Birthday" xml:space="preserve"><value>Anniversaire</value></data>
|
||||
<data name="Home.MustBeLoggedIn" xml:space="preserve"><value>Vous devez être connecté pour créer une liste.</value></data>
|
||||
<data name="Home.TitleRequired" xml:space="preserve"><value>Le titre est obligatoire.</value></data>
|
||||
<data name="Home.LoginPrompt" xml:space="preserve"><value>Veuillez vous connecter pour créer et gérer des listes.</value></data>
|
||||
<data name="Home.LoginPromptPrefix" xml:space="preserve"><value>Veuillez </value></data>
|
||||
<data name="Home.LoginPromptLinkText" xml:space="preserve"><value>vous connecter</value></data>
|
||||
<data name="Home.LoginPromptSuffix" xml:space="preserve"><value> pour créer et gérer des listes.</value></data>
|
||||
|
||||
<data name="RegistryPublic.PageTitle" xml:space="preserve"><value>Liste</value></data>
|
||||
<data name="RegistryPublic.NotFound" xml:space="preserve"><value>Liste introuvable.</value></data>
|
||||
<data name="RegistryPublic.GoToAdmin" xml:space="preserve"><value>Aller à l'admin</value></data>
|
||||
<data name="RegistryPublic.ShippingAddress" xml:space="preserve"><value>Adresse de livraison</value></data>
|
||||
<data name="RegistryPublic.BankTransferParticipation" xml:space="preserve"><value>Participation par virement</value></data>
|
||||
<data name="RegistryPublic.IBAN" xml:space="preserve"><value>IBAN</value></data>
|
||||
<data name="RegistryPublic.BIC" xml:space="preserve"><value>BIC</value></data>
|
||||
<data name="RegistryPublic.SecondHandPreferred" xml:space="preserve"><value>Seconde main privilégiée</value></data>
|
||||
<data name="RegistryPublic.SecondHandOptional" xml:space="preserve"><value>Seconde main possible</value></data>
|
||||
<data name="RegistryPublic.Qty" xml:space="preserve"><value>Qté :</value></data>
|
||||
<data name="RegistryPublic.PurchasedSuffix" xml:space="preserve"><value>achetés</value></data>
|
||||
<data name="RegistryPublic.Price" xml:space="preserve"><value>Prix :</value></data>
|
||||
<data name="RegistryPublic.Participation" xml:space="preserve"><value>Participation :</value></data>
|
||||
<data name="RegistryPublic.OutOfFulfilled" xml:space="preserve"><value>sur {0}{1} atteints</value></data>
|
||||
<data name="RegistryPublic.FulfilledOnly" xml:space="preserve"><value>atteints</value></data>
|
||||
<data name="RegistryPublic.PurchasedBy" xml:space="preserve"><value>Acheté par :</value></data>
|
||||
<data name="RegistryPublic.ContributedBy" xml:space="preserve"><value>Contribué par :</value></data>
|
||||
<data name="RegistryPublic.YouQuantity" xml:space="preserve"><value>Vous ({0})</value></data>
|
||||
<data name="RegistryPublic.AndOtherPeople" xml:space="preserve"><value>et {0} autre{1}</value></data>
|
||||
<data name="RegistryPublic.Person" xml:space="preserve"><value> personne</value></data>
|
||||
<data name="RegistryPublic.People" xml:space="preserve"><value>s personnes</value></data>
|
||||
<data name="RegistryPublic.AndOthers" xml:space="preserve"><value>et d'autres</value></data>
|
||||
<data name="RegistryPublic.Purchased" xml:space="preserve"><value>Acheté</value></data>
|
||||
<data name="RegistryPublic.Contributed" xml:space="preserve"><value>Contribué</value></data>
|
||||
<data name="RegistryPublic.LoginToPurchase" xml:space="preserve"><value>Connectez-vous pour acheter</value></data>
|
||||
<data name="RegistryPublic.Purchase" xml:space="preserve"><value>Acheter</value></data>
|
||||
<data name="RegistryPublic.ManagePurchases" xml:space="preserve"><value>Gérer les achats</value></data>
|
||||
<data name="RegistryPublic.EditPurchase" xml:space="preserve"><value>Modifier l'achat</value></data>
|
||||
<data name="RegistryPublic.MarkPurchased" xml:space="preserve"><value>Marquer comme acheté</value></data>
|
||||
<data name="RegistryPublic.ManageParticipations" xml:space="preserve"><value>Gérer les participations</value></data>
|
||||
<data name="RegistryPublic.EditParticipation" xml:space="preserve"><value>Modifier la participation</value></data>
|
||||
<data name="RegistryPublic.PartiallyFulfill" xml:space="preserve"><value>Participer partiellement</value></data>
|
||||
<data name="RegistryPublic.MarkAsPurchased" xml:space="preserve"><value>Marquer comme acheté</value></data>
|
||||
<data name="RegistryPublic.HowManyUnitsPurchased" xml:space="preserve"><value>Combien d'unités avez-vous achetées ?</value></data>
|
||||
<data name="RegistryPublic.OpenProductLink" xml:space="preserve"><value>Ouvrir le lien du produit</value></data>
|
||||
<data name="RegistryPublic.LogContribution" xml:space="preserve"><value>Enregistrer la contribution</value></data>
|
||||
<data name="RegistryPublic.TransferredAmount" xml:space="preserve"><value>Montant transféré</value></data>
|
||||
<data name="RegistryPublic.Confirm" xml:space="preserve"><value>Confirmer</value></data>
|
||||
<data name="RegistryPublic.ConfirmPurchase" xml:space="preserve"><value>Confirmer l'achat</value></data>
|
||||
<data name="RegistryPublic.SelectPurchaserToUnmark" xml:space="preserve"><value>Sélectionner l'acheteur à annuler</value></data>
|
||||
<data name="RegistryPublic.MultipleUsersPurchased" xml:space="preserve"><value>Plusieurs utilisateurs ont acheté cet article. Choisissez l'achat à annuler :</value></data>
|
||||
<data name="RegistryPublic.Unmark" xml:space="preserve"><value>Annuler</value></data>
|
||||
<data name="RegistryPublic.UnmarkAll" xml:space="preserve"><value>Tout annuler</value></data>
|
||||
<data name="RegistryPublic.PartiallyFulfillItem" xml:space="preserve"><value>Participer partiellement à l'article</value></data>
|
||||
<data name="RegistryPublic.SelectDonateMethod" xml:space="preserve"><value>Choisissez votre mode de participation :</value></data>
|
||||
<data name="RegistryPublic.IbanTransfer" xml:space="preserve"><value>Virement IBAN</value></data>
|
||||
<data name="RegistryPublic.SingleQrCode" xml:space="preserve"><value>QR code unique</value></data>
|
||||
<data name="RegistryPublic.QrCodePerAmount" xml:space="preserve"><value>QR code par montant</value></data>
|
||||
<data name="RegistryPublic.OpenPaymentLink" xml:space="preserve"><value>Ouvrir le lien de paiement</value></data>
|
||||
<data name="RegistryPublic.HowMuchAdded" xml:space="preserve"><value>Quel montant avez-vous ajouté pour cet article ?</value></data>
|
||||
<data name="RegistryPublic.NoQrForAmount" xml:space="preserve"><value>Aucun QR code configuré pour ce montant exact.</value></data>
|
||||
<data name="RegistryPublic.ManagePurchase" xml:space="preserve"><value>Gérer l'achat</value></data>
|
||||
<data name="RegistryPublic.ManageParticipation" xml:space="preserve"><value>Gérer la participation</value></data>
|
||||
<data name="RegistryPublic.User" xml:space="preserve"><value>Utilisateur</value></data>
|
||||
|
||||
<data name="RegistryContributionAmount.PageTitle" xml:space="preserve"><value>Montant de participation</value></data>
|
||||
<data name="RegistryContributionAmount.ItemNotFound" xml:space="preserve"><value>Article introuvable.</value></data>
|
||||
<data name="RegistryContributionAmount.LoginFirst" xml:space="preserve"><value>Veuillez d'abord vous connecter.</value></data>
|
||||
<data name="RegistryContributionAmount.PartiallyFulfill" xml:space="preserve"><value>Participation partielle : {0}</value></data>
|
||||
<data name="RegistryContributionAmount.NoRepresentableAmount" xml:space="preserve"><value>Aucun montant représentable ne peut être formé avec les QR codes configurés jusqu'à {0}200.</value></data>
|
||||
<data name="RegistryContributionAmount.SelectAmount" xml:space="preserve"><value>Sélectionnez un montant : {0}{1}</value></data>
|
||||
<data name="RegistryContributionAmount.NoQrCombination" xml:space="preserve"><value>Aucune combinaison QR disponible pour ce montant.</value></data>
|
||||
<data name="RegistryContributionAmount.SuggestedCombination" xml:space="preserve"><value>Combinaison QR suggérée :</value></data>
|
||||
<data name="RegistryContributionAmount.OpenPaymentLink" xml:space="preserve"><value>Ouvrir le lien de paiement</value></data>
|
||||
<data name="RegistryContributionAmount.TransferredAmount" xml:space="preserve"><value>J'ai transféré ce montant</value></data>
|
||||
|
||||
<data name="RegistryInvite.PageTitle" xml:space="preserve"><value>Invitation admin</value></data>
|
||||
<data name="RegistryInvite.Title" xml:space="preserve"><value>Invitation administrateur</value></data>
|
||||
<data name="RegistryInvite.Validating" xml:space="preserve"><value>Validation de l'invitation...</value></data>
|
||||
<data name="RegistryInvite.Accepted" xml:space="preserve"><value>Invitation acceptée. Vous êtes maintenant administrateur.</value></data>
|
||||
<data name="RegistryInvite.GoToAdmin" xml:space="preserve"><value>Aller à l'administration</value></data>
|
||||
<data name="RegistryInvite.Invalid" xml:space="preserve"><value>L'invitation est invalide ou déjà utilisée.</value></data>
|
||||
|
||||
<data name="RegistryAdmin.PageTitle" xml:space="preserve"><value>Administration de la liste</value></data>
|
||||
<data name="RegistryAdmin.Title" xml:space="preserve"><value>Administration de la liste</value></data>
|
||||
<data name="RegistryAdmin.SmtpNotConfigured" xml:space="preserve"><value>SMTP n'est pas configuré. Les fonctionnalités e-mail (Identity et invitations admin) sont désactivées. Configurez la section Smtp dans appsettings ou user secrets.</value></data>
|
||||
<data name="RegistryAdmin.Tab.Items" xml:space="preserve"><value>Articles</value></data>
|
||||
<data name="RegistryAdmin.Tab.Settings" xml:space="preserve"><value>Paramètres</value></data>
|
||||
<data name="RegistryAdmin.Tab.Administrators" xml:space="preserve"><value>Administrateurs</value></data>
|
||||
<data name="RegistryAdmin.Tab.Addresses" xml:space="preserve"><value>Adresses</value></data>
|
||||
<data name="RegistryAdmin.Tab.ActionLog" xml:space="preserve"><value>Journal d'actions</value></data>
|
||||
<data name="RegistryAdmin.ViewPublicList" xml:space="preserve"><value>Voir la liste publique</value></data>
|
||||
<data name="RegistryAdmin.AddOrEditItem" xml:space="preserve"><value>Ajouter ou modifier un article</value></data>
|
||||
<data name="RegistryAdmin.Name" xml:space="preserve"><value>Nom</value></data>
|
||||
<data name="RegistryAdmin.ProductUrl" xml:space="preserve"><value>URL du produit</value></data>
|
||||
<data name="RegistryAdmin.AutoFetch" xml:space="preserve"><value>Remplissage auto</value></data>
|
||||
<data name="RegistryAdmin.PictureUrl" xml:space="preserve"><value>URL de l'image</value></data>
|
||||
<data name="RegistryAdmin.Description" xml:space="preserve"><value>Description</value></data>
|
||||
<data name="RegistryAdmin.Price" xml:space="preserve"><value>Prix</value></data>
|
||||
<data name="RegistryAdmin.CurrencySymbol" xml:space="preserve"><value>Symbole monétaire</value></data>
|
||||
<data name="RegistryAdmin.DesiredQty" xml:space="preserve"><value>Quantité souhaitée</value></data>
|
||||
<data name="RegistryAdmin.Participation" xml:space="preserve"><value>Participation</value></data>
|
||||
<data name="RegistryAdmin.ParticipationTarget" xml:space="preserve"><value>Objectif de participation</value></data>
|
||||
<data name="RegistryAdmin.SecondHandPreference" xml:space="preserve"><value>Préférence seconde main</value></data>
|
||||
<data name="RegistryAdmin.SecondHandOptional" xml:space="preserve"><value>Seconde main possible</value></data>
|
||||
<data name="RegistryAdmin.PreferSecondHand" xml:space="preserve"><value>Préférer la seconde main</value></data>
|
||||
<data name="RegistryAdmin.NewOnly" xml:space="preserve"><value>Neuf uniquement</value></data>
|
||||
<data name="RegistryAdmin.Given" xml:space="preserve"><value>Donné</value></data>
|
||||
<data name="RegistryAdmin.Category" xml:space="preserve"><value>Catégorie</value></data>
|
||||
<data name="RegistryAdmin.SaveItem" xml:space="preserve"><value>Enregistrer l'article</value></data>
|
||||
<data name="RegistryAdmin.CategoriesAndItems" xml:space="preserve"><value>Catégories et articles</value></data>
|
||||
<data name="RegistryAdmin.NewCategory" xml:space="preserve"><value>Nouvelle catégorie</value></data>
|
||||
<data name="RegistryAdmin.AddCategory" xml:space="preserve"><value>Ajouter une catégorie</value></data>
|
||||
<data name="RegistryAdmin.DragHint" xml:space="preserve"><value>Faites glisser les catégories ou les articles pour réorganiser. Déposez les articles dans une autre catégorie pour les regrouper.</value></data>
|
||||
<data name="RegistryAdmin.Rename" xml:space="preserve"><value>Renommer</value></data>
|
||||
<data name="RegistryAdmin.DropItemsHere" xml:space="preserve"><value>Déposez des articles ici.</value></data>
|
||||
<data name="RegistryAdmin.Header.Name" xml:space="preserve"><value>Nom</value></data>
|
||||
<data name="RegistryAdmin.Header.DesiredQty" xml:space="preserve"><value>Qté souhaitée</value></data>
|
||||
<data name="RegistryAdmin.Header.Condition" xml:space="preserve"><value>État</value></data>
|
||||
<data name="RegistryAdmin.Header.Participation" xml:space="preserve"><value>Participation</value></data>
|
||||
<data name="RegistryAdmin.Header.PurchasedContributedBy" xml:space="preserve"><value>Acheté par / Contribué par</value></data>
|
||||
<data name="RegistryAdmin.Purchased" xml:space="preserve"><value>Acheté :</value></data>
|
||||
<data name="RegistryAdmin.Contributed" xml:space="preserve"><value>Contribué :</value></data>
|
||||
<data name="RegistryAdmin.RegistrySettings" xml:space="preserve"><value>Paramètres de la liste</value></data>
|
||||
<data name="RegistryAdmin.BabyName" xml:space="preserve"><value>Prénom du bébé</value></data>
|
||||
<data name="RegistryAdmin.BirthDate" xml:space="preserve"><value>Date de naissance</value></data>
|
||||
<data name="RegistryAdmin.Theme" xml:space="preserve"><value>Thème</value></data>
|
||||
<data name="RegistryAdmin.Theme.Default" xml:space="preserve"><value>Par défaut</value></data>
|
||||
<data name="RegistryAdmin.Theme.Soft" xml:space="preserve"><value>Doux</value></data>
|
||||
<data name="RegistryAdmin.Theme.Modern" xml:space="preserve"><value>Moderne</value></data>
|
||||
<data name="RegistryAdmin.ShippingAddress" xml:space="preserve"><value>Adresse de livraison</value></data>
|
||||
<data name="RegistryAdmin.LineBreaksPreserved" xml:space="preserve"><value>Les sauts de ligne seront conservés</value></data>
|
||||
<data name="RegistryAdmin.HeroImage" xml:space="preserve"><value>Image héros</value></data>
|
||||
<data name="RegistryAdmin.HideHeaderName" xml:space="preserve"><value>Masquer le titre du nom sur la page publique</value></data>
|
||||
<data name="RegistryAdmin.UploadImage" xml:space="preserve"><value>Télécharger une image</value></data>
|
||||
<data name="RegistryAdmin.RemoveHeroImage" xml:space="preserve"><value>Supprimer l'image héros</value></data>
|
||||
<data name="RegistryAdmin.CurrentHeroImage" xml:space="preserve"><value>Image héros actuelle</value></data>
|
||||
<data name="RegistryAdmin.TopContent" xml:space="preserve"><value>Contenu d'en-tête</value></data>
|
||||
<data name="RegistryAdmin.WelcomeText" xml:space="preserve"><value>Texte d'accueil</value></data>
|
||||
<data name="RegistryAdmin.BankAccountSettings" xml:space="preserve"><value>Paramètres du compte bancaire</value></data>
|
||||
<data name="RegistryAdmin.BankAccountName" xml:space="preserve"><value>Nom du compte bancaire</value></data>
|
||||
<data name="RegistryAdmin.DisplayBankAccountName" xml:space="preserve"><value>Afficher le nom du compte bancaire</value></data>
|
||||
<data name="RegistryAdmin.ContributionPaymentOptions" xml:space="preserve"><value>Options de paiement des participations</value></data>
|
||||
<data name="RegistryAdmin.SingleQrCodeUrl" xml:space="preserve"><value>URL du QR code unique</value></data>
|
||||
<data name="RegistryAdmin.SingleQrHelp" xml:space="preserve"><value>Optionnel : un QR code unique que les donateurs peuvent scanner pour n'importe quel montant.</value></data>
|
||||
<data name="RegistryAdmin.AmountSpecificQrCodes" xml:space="preserve"><value>QR codes par montant</value></data>
|
||||
<data name="RegistryAdmin.AddQrAmount" xml:space="preserve"><value>Ajouter un montant QR</value></data>
|
||||
<data name="RegistryAdmin.NoAmountSpecificQrCodes" xml:space="preserve"><value>Aucun QR code par montant configuré.</value></data>
|
||||
<data name="RegistryAdmin.QrCodeUrl" xml:space="preserve"><value>URL du QR code</value></data>
|
||||
<data name="RegistryAdmin.SaveSettings" xml:space="preserve"><value>Enregistrer les paramètres</value></data>
|
||||
<data name="RegistryAdmin.UserAddresses" xml:space="preserve"><value>Adresses des utilisateurs</value></data>
|
||||
<data name="RegistryAdmin.NoUsersFound" xml:space="preserve"><value>Aucun utilisateur trouvé pour le moment.</value></data>
|
||||
<data name="RegistryAdmin.Email" xml:space="preserve"><value>E-mail</value></data>
|
||||
<data name="RegistryAdmin.Address" xml:space="preserve"><value>Adresse</value></data>
|
||||
<data name="RegistryAdmin.CurrentAdministrators" xml:space="preserve"><value>Administrateurs actuels</value></data>
|
||||
<data name="RegistryAdmin.NoAdmins" xml:space="preserve"><value>Aucun administrateur assigné pour le moment.</value></data>
|
||||
<data name="RegistryAdmin.EmailOrName" xml:space="preserve"><value>E-mail / Nom</value></data>
|
||||
<data name="RegistryAdmin.InviteAdministrator" xml:space="preserve"><value>Inviter un administrateur</value></data>
|
||||
<data name="RegistryAdmin.OptionalEmail" xml:space="preserve"><value>e-mail optionnel</value></data>
|
||||
<data name="RegistryAdmin.CreateInvite" xml:space="preserve"><value>Créer une invitation</value></data>
|
||||
<data name="RegistryAdmin.InviteLink" xml:space="preserve"><value>Lien d'invitation :</value></data>
|
||||
|
||||
<data name="RegistryActionLog.PageTitle" xml:space="preserve"><value>Journal d'actions - Admin liste</value></data>
|
||||
<data name="RegistryActionLog.Title" xml:space="preserve"><value>Journal d'actions de la liste</value></data>
|
||||
<data name="RegistryActionLog.Description" xml:space="preserve"><value>Ce journal montre toutes les actions des utilisateurs sur cette liste : achats, participations et autres interactions.</value></data>
|
||||
<data name="RegistryActionLog.NoActions" xml:space="preserve"><value>Aucune action enregistrée pour le moment.</value></data>
|
||||
<data name="RegistryActionLog.DateTime" xml:space="preserve"><value>Date/Heure</value></data>
|
||||
<data name="RegistryActionLog.User" xml:space="preserve"><value>Utilisateur</value></data>
|
||||
<data name="RegistryActionLog.Action" xml:space="preserve"><value>Action</value></data>
|
||||
<data name="RegistryActionLog.Item" xml:space="preserve"><value>Article</value></data>
|
||||
<data name="RegistryActionLog.Quantity" xml:space="preserve"><value>Quantité</value></data>
|
||||
<data name="RegistryActionLog.Amount" xml:space="preserve"><value>Montant</value></data>
|
||||
<data name="RegistryActionLog.Details" xml:space="preserve"><value>Détails</value></data>
|
||||
<data name="RegistryActionLog.Badge.RegistryOpened" xml:space="preserve"><value>Liste ouverte</value></data>
|
||||
<data name="RegistryActionLog.Badge.ItemLinkOpened" xml:space="preserve"><value>Lien article ouvert</value></data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseMarked" xml:space="preserve"><value>Achat marqué</value></data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve"><value>Achat annulé</value></data>
|
||||
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve"><value>Achat partiel</value></data>
|
||||
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve"><value>Participation enregistrée</value></data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve"><value>Remplissage auto réussi</value></data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve"><value>Remplissage auto échoué</value></data>
|
||||
|
||||
<data name="Error.PageTitle" xml:space="preserve"><value>Erreur</value></data>
|
||||
<data name="Error.Title" xml:space="preserve"><value>Erreur.</value></data>
|
||||
<data name="Error.Subtitle" xml:space="preserve"><value>Une erreur est survenue lors du traitement de votre demande.</value></data>
|
||||
<data name="Error.RequestId" xml:space="preserve"><value>ID de requête :</value></data>
|
||||
<data name="Error.DevMode" xml:space="preserve"><value>Mode Développement</value></data>
|
||||
<data name="Error.DevHint1" xml:space="preserve"><value>Passer en environnement Development affichera des informations plus détaillées sur l'erreur.</value></data>
|
||||
<data name="Error.DevHint2" xml:space="preserve"><value>L'environnement Development ne devrait pas être activé sur une application déployée.</value></data>
|
||||
<data name="Error.DevHint3" xml:space="preserve"><value>Cela peut afficher des informations sensibles issues des exceptions aux utilisateurs finaux.</value></data>
|
||||
<data name="Error.DevHint4" xml:space="preserve"><value>Pour le débogage local, activez l'environnement Development en définissant ASPNETCORE_ENVIRONMENT à Development puis redémarrez l'application.</value></data>
|
||||
|
||||
<data name="MainLayout.UnhandledError" xml:space="preserve"><value>Une erreur non gérée est survenue.</value></data>
|
||||
<data name="MainLayout.Reload" xml:space="preserve"><value>Recharger</value></data>
|
||||
|
||||
<data name="NavMenu.Brand" xml:space="preserve"><value>BirthList</value></data>
|
||||
<data name="NavMenu.NavigationMenu" xml:space="preserve"><value>Menu de navigation</value></data>
|
||||
<data name="NavMenu.Home" xml:space="preserve"><value>Accueil</value></data>
|
||||
<data name="NavMenu.Logout" xml:space="preserve"><value>Se déconnecter</value></data>
|
||||
<data name="NavMenu.Register" xml:space="preserve"><value>S'inscrire</value></data>
|
||||
<data name="NavMenu.Login" xml:space="preserve"><value>Se connecter</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype"><value>text/microsoft-resx</value></resheader>
|
||||
<resheader name="version"><value>2.0</value></resheader>
|
||||
<resheader name="reader"><value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value></resheader>
|
||||
<resheader name="writer"><value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value></resheader>
|
||||
|
||||
<data name="Common.Yes" xml:space="preserve"><value>Ja</value></data>
|
||||
<data name="Common.No" xml:space="preserve"><value>Nee</value></data>
|
||||
<data name="Common.Save" xml:space="preserve"><value>Opslaan</value></data>
|
||||
<data name="Common.Cancel" xml:space="preserve"><value>Annuleren</value></data>
|
||||
<data name="Common.Remove" xml:space="preserve"><value>Verwijderen</value></data>
|
||||
<data name="Common.Edit" xml:space="preserve"><value>Bewerken</value></data>
|
||||
<data name="Common.Back" xml:space="preserve"><value>Terug</value></data>
|
||||
<data name="Common.Next" xml:space="preserve"><value>Volgende</value></data>
|
||||
<data name="Common.Loading" xml:space="preserve"><value>Laden...</value></data>
|
||||
<data name="Common.AccessDenied" xml:space="preserve"><value>Toegang geweigerd.</value></data>
|
||||
<data name="Common.SelectUser" xml:space="preserve"><value>Selecteer gebruiker</value></data>
|
||||
<data name="Common.SearchUser" xml:space="preserve"><value>Zoek gebruiker</value></data>
|
||||
<data name="Common.Quantity" xml:space="preserve"><value>Aantal</value></data>
|
||||
<data name="Common.Amount" xml:space="preserve"><value>Bedrag</value></data>
|
||||
<data name="Common.Message" xml:space="preserve"><value>Bericht</value></data>
|
||||
|
||||
<data name="TopBar.Language" xml:space="preserve"><value>Taal</value></data>
|
||||
<data name="TopBar.Brand" xml:space="preserve"><value>Gift List</value></data>
|
||||
<data name="TopBar.ProfilePrompt" xml:space="preserve"><value>Vul je profiel aan (voornaam, achternaam en adres).</value></data>
|
||||
<data name="TopBar.CompleteProfile" xml:space="preserve"><value>Profiel aanvullen</value></data>
|
||||
<data name="TopBar.AccountSettings" xml:space="preserve"><value>Accountinstellingen</value></data>
|
||||
<data name="TopBar.SignIn" xml:space="preserve"><value>Aanmelden</value></data>
|
||||
<data name="TopBar.SignOut" xml:space="preserve"><value>Afmelden</value></data>
|
||||
|
||||
<data name="Home.PageTitle" xml:space="preserve"><value>Geboortelijst</value></data>
|
||||
<data name="Home.Welcome" xml:space="preserve"><value>Welkom bij Gift List</value></data>
|
||||
<data name="Home.ManagedRegistries" xml:space="preserve"><value>Lijsten die jij beheert</value></data>
|
||||
<data name="Home.CreateNew" xml:space="preserve"><value>Nieuwe maken</value></data>
|
||||
<data name="Home.NoRegistries" xml:space="preserve"><value>Nog geen lijsten.</value></data>
|
||||
<data name="Home.View" xml:space="preserve"><value>Bekijken</value></data>
|
||||
<data name="Home.Manage" xml:space="preserve"><value>Beheren</value></data>
|
||||
<data name="Home.VisitedRegistries" xml:space="preserve"><value>Bezochte lijsten</value></data>
|
||||
<data name="Home.NoVisitedRegistries" xml:space="preserve"><value>Nog geen bezochte lijsten.</value></data>
|
||||
<data name="Home.CreateRegistryTitle" xml:space="preserve"><value>Nieuwe lijst aanmaken</value></data>
|
||||
<data name="Home.Title" xml:space="preserve"><value>Titel</value></data>
|
||||
<data name="Home.Type" xml:space="preserve"><value>Type</value></data>
|
||||
<data name="Home.Theme" xml:space="preserve"><value>Thema</value></data>
|
||||
<data name="Home.Theme.Default" xml:space="preserve"><value>Standaard</value></data>
|
||||
<data name="Home.Theme.Soft" xml:space="preserve"><value>Zacht</value></data>
|
||||
<data name="Home.Theme.Modern" xml:space="preserve"><value>Modern</value></data>
|
||||
<data name="Home.RegistryType.Birth" xml:space="preserve"><value>Geboorte</value></data>
|
||||
<data name="Home.RegistryType.Wedding" xml:space="preserve"><value>Huwelijk</value></data>
|
||||
<data name="Home.RegistryType.Birthday" xml:space="preserve"><value>Verjaardag</value></data>
|
||||
<data name="Home.MustBeLoggedIn" xml:space="preserve"><value>Je moet aangemeld zijn om een lijst aan te maken.</value></data>
|
||||
<data name="Home.TitleRequired" xml:space="preserve"><value>Titel is verplicht.</value></data>
|
||||
<data name="Home.LoginPrompt" xml:space="preserve"><value>Meld je aan om lijsten aan te maken en te beheren.</value></data>
|
||||
<data name="Home.LoginPromptPrefix" xml:space="preserve"><value>Gelieve </value></data>
|
||||
<data name="Home.LoginPromptLinkText" xml:space="preserve"><value>aan te melden</value></data>
|
||||
<data name="Home.LoginPromptSuffix" xml:space="preserve"><value> om lijsten aan te maken en te beheren.</value></data>
|
||||
|
||||
<data name="RegistryPublic.PageTitle" xml:space="preserve"><value>Lijst</value></data>
|
||||
<data name="RegistryPublic.NotFound" xml:space="preserve"><value>Lijst niet gevonden.</value></data>
|
||||
<data name="RegistryPublic.GoToAdmin" xml:space="preserve"><value>Naar beheer</value></data>
|
||||
<data name="RegistryPublic.ShippingAddress" xml:space="preserve"><value>Leveringsadres</value></data>
|
||||
<data name="RegistryPublic.BankTransferParticipation" xml:space="preserve"><value>Deelname via overschrijving</value></data>
|
||||
<data name="RegistryPublic.IBAN" xml:space="preserve"><value>IBAN</value></data>
|
||||
<data name="RegistryPublic.BIC" xml:space="preserve"><value>BIC</value></data>
|
||||
<data name="RegistryPublic.SecondHandPreferred" xml:space="preserve"><value>Tweedehands heeft de voorkeur</value></data>
|
||||
<data name="RegistryPublic.SecondHandOptional" xml:space="preserve"><value>Tweedehands is mogelijk</value></data>
|
||||
<data name="RegistryPublic.Qty" xml:space="preserve"><value>Aantal:</value></data>
|
||||
<data name="RegistryPublic.PurchasedSuffix" xml:space="preserve"><value>gekocht</value></data>
|
||||
<data name="RegistryPublic.Price" xml:space="preserve"><value>Prijs:</value></data>
|
||||
<data name="RegistryPublic.Participation" xml:space="preserve"><value>Deelname:</value></data>
|
||||
<data name="RegistryPublic.OutOfFulfilled" xml:space="preserve"><value>van {0}{1} behaald</value></data>
|
||||
<data name="RegistryPublic.FulfilledOnly" xml:space="preserve"><value>behaald</value></data>
|
||||
<data name="RegistryPublic.PurchasedBy" xml:space="preserve"><value>Gekocht door:</value></data>
|
||||
<data name="RegistryPublic.ContributedBy" xml:space="preserve"><value>Bijgedragen door:</value></data>
|
||||
<data name="RegistryPublic.YouQuantity" xml:space="preserve"><value>Jij ({0})</value></data>
|
||||
<data name="RegistryPublic.AndOtherPeople" xml:space="preserve"><value>en {0} andere {1}</value></data>
|
||||
<data name="RegistryPublic.Person" xml:space="preserve"><value>persoon</value></data>
|
||||
<data name="RegistryPublic.People" xml:space="preserve"><value>personen</value></data>
|
||||
<data name="RegistryPublic.AndOthers" xml:space="preserve"><value>en anderen</value></data>
|
||||
<data name="RegistryPublic.Purchased" xml:space="preserve"><value>Gekocht</value></data>
|
||||
<data name="RegistryPublic.Contributed" xml:space="preserve"><value>Bijgedragen</value></data>
|
||||
<data name="RegistryPublic.LoginToPurchase" xml:space="preserve"><value>Meld je aan om te kopen</value></data>
|
||||
<data name="RegistryPublic.Purchase" xml:space="preserve"><value>Kopen</value></data>
|
||||
<data name="RegistryPublic.ManagePurchases" xml:space="preserve"><value>Aankopen beheren</value></data>
|
||||
<data name="RegistryPublic.EditPurchase" xml:space="preserve"><value>Aankoop bewerken</value></data>
|
||||
<data name="RegistryPublic.MarkPurchased" xml:space="preserve"><value>Markeer als gekocht</value></data>
|
||||
<data name="RegistryPublic.ManageParticipations" xml:space="preserve"><value>Deelnames beheren</value></data>
|
||||
<data name="RegistryPublic.EditParticipation" xml:space="preserve"><value>Deelname bewerken</value></data>
|
||||
<data name="RegistryPublic.PartiallyFulfill" xml:space="preserve"><value>Gedeeltelijk vervullen</value></data>
|
||||
<data name="RegistryPublic.MarkAsPurchased" xml:space="preserve"><value>Markeer als gekocht</value></data>
|
||||
<data name="RegistryPublic.HowManyUnitsPurchased" xml:space="preserve"><value>Hoeveel stuks heb je gekocht?</value></data>
|
||||
<data name="RegistryPublic.OpenProductLink" xml:space="preserve"><value>Open productlink</value></data>
|
||||
<data name="RegistryPublic.LogContribution" xml:space="preserve"><value>Bijdrage registreren</value></data>
|
||||
<data name="RegistryPublic.TransferredAmount" xml:space="preserve"><value>Overgeschreven bedrag</value></data>
|
||||
<data name="RegistryPublic.Confirm" xml:space="preserve"><value>Bevestigen</value></data>
|
||||
<data name="RegistryPublic.ConfirmPurchase" xml:space="preserve"><value>Aankoop bevestigen</value></data>
|
||||
<data name="RegistryPublic.SelectPurchaserToUnmark" xml:space="preserve"><value>Selecteer koper om te verwijderen</value></data>
|
||||
<data name="RegistryPublic.MultipleUsersPurchased" xml:space="preserve"><value>Meerdere gebruikers hebben dit item gekocht. Kies welke aankoop je wil verwijderen:</value></data>
|
||||
<data name="RegistryPublic.Unmark" xml:space="preserve"><value>Verwijderen</value></data>
|
||||
<data name="RegistryPublic.UnmarkAll" xml:space="preserve"><value>Alles verwijderen</value></data>
|
||||
<data name="RegistryPublic.PartiallyFulfillItem" xml:space="preserve"><value>Item gedeeltelijk vervullen</value></data>
|
||||
<data name="RegistryPublic.SelectDonateMethod" xml:space="preserve"><value>Kies hoe je wil bijdragen:</value></data>
|
||||
<data name="RegistryPublic.IbanTransfer" xml:space="preserve"><value>IBAN-overschrijving</value></data>
|
||||
<data name="RegistryPublic.SingleQrCode" xml:space="preserve"><value>Enkele QR-code</value></data>
|
||||
<data name="RegistryPublic.QrCodePerAmount" xml:space="preserve"><value>QR-code per bedrag</value></data>
|
||||
<data name="RegistryPublic.OpenPaymentLink" xml:space="preserve"><value>Open betaallink</value></data>
|
||||
<data name="RegistryPublic.HowMuchAdded" xml:space="preserve"><value>Hoeveel heb je toegevoegd voor dit item?</value></data>
|
||||
<data name="RegistryPublic.NoQrForAmount" xml:space="preserve"><value>Geen QR-code ingesteld voor dit exacte bedrag.</value></data>
|
||||
<data name="RegistryPublic.ManagePurchase" xml:space="preserve"><value>Aankoop beheren</value></data>
|
||||
<data name="RegistryPublic.ManageParticipation" xml:space="preserve"><value>Deelname beheren</value></data>
|
||||
<data name="RegistryPublic.User" xml:space="preserve"><value>Gebruiker</value></data>
|
||||
|
||||
<data name="RegistryContributionAmount.PageTitle" xml:space="preserve"><value>Deelnamebedrag</value></data>
|
||||
<data name="RegistryContributionAmount.ItemNotFound" xml:space="preserve"><value>Item niet gevonden.</value></data>
|
||||
<data name="RegistryContributionAmount.LoginFirst" xml:space="preserve"><value>Meld je eerst aan.</value></data>
|
||||
<data name="RegistryContributionAmount.PartiallyFulfill" xml:space="preserve"><value>Gedeeltelijk vervullen: {0}</value></data>
|
||||
<data name="RegistryContributionAmount.NoRepresentableAmount" xml:space="preserve"><value>Er kan geen voorstelbaar bedrag gevormd worden met ingestelde QR-codes tot {0}200.</value></data>
|
||||
<data name="RegistryContributionAmount.SelectAmount" xml:space="preserve"><value>Selecteer bedrag: {0}{1}</value></data>
|
||||
<data name="RegistryContributionAmount.NoQrCombination" xml:space="preserve"><value>Geen QR-combinatie beschikbaar voor dit bedrag.</value></data>
|
||||
<data name="RegistryContributionAmount.SuggestedCombination" xml:space="preserve"><value>Voorgestelde QR-combinatie:</value></data>
|
||||
<data name="RegistryContributionAmount.OpenPaymentLink" xml:space="preserve"><value>Open betaallink</value></data>
|
||||
<data name="RegistryContributionAmount.TransferredAmount" xml:space="preserve"><value>Ik heb dit bedrag overgeschreven</value></data>
|
||||
|
||||
<data name="RegistryInvite.PageTitle" xml:space="preserve"><value>Admin-uitnodiging</value></data>
|
||||
<data name="RegistryInvite.Title" xml:space="preserve"><value>Uitnodiging voor beheerder</value></data>
|
||||
<data name="RegistryInvite.Validating" xml:space="preserve"><value>Uitnodiging valideren...</value></data>
|
||||
<data name="RegistryInvite.Accepted" xml:space="preserve"><value>Uitnodiging geaccepteerd. Je bent nu beheerder.</value></data>
|
||||
<data name="RegistryInvite.GoToAdmin" xml:space="preserve"><value>Ga naar beheer</value></data>
|
||||
<data name="RegistryInvite.Invalid" xml:space="preserve"><value>De uitnodiging is ongeldig of al gebruikt.</value></data>
|
||||
|
||||
<data name="RegistryAdmin.PageTitle" xml:space="preserve"><value>Lijstbeheer</value></data>
|
||||
<data name="RegistryAdmin.Title" xml:space="preserve"><value>Lijstbeheer</value></data>
|
||||
<data name="RegistryAdmin.SmtpNotConfigured" xml:space="preserve"><value>SMTP is niet geconfigureerd. E-mailfuncties (Identity-mails en admin-uitnodigingen) zijn uitgeschakeld. Configureer de sectie Smtp in appsettings of user secrets.</value></data>
|
||||
<data name="RegistryAdmin.Tab.Items" xml:space="preserve"><value>Items</value></data>
|
||||
<data name="RegistryAdmin.Tab.Settings" xml:space="preserve"><value>Instellingen</value></data>
|
||||
<data name="RegistryAdmin.Tab.Administrators" xml:space="preserve"><value>Beheerders</value></data>
|
||||
<data name="RegistryAdmin.Tab.Addresses" xml:space="preserve"><value>Adressen</value></data>
|
||||
<data name="RegistryAdmin.Tab.ActionLog" xml:space="preserve"><value>Actielog</value></data>
|
||||
<data name="RegistryAdmin.ViewPublicList" xml:space="preserve"><value>Openbare lijst bekijken</value></data>
|
||||
<data name="RegistryAdmin.AddOrEditItem" xml:space="preserve"><value>Item toevoegen of bewerken</value></data>
|
||||
<data name="RegistryAdmin.Name" xml:space="preserve"><value>Naam</value></data>
|
||||
<data name="RegistryAdmin.ProductUrl" xml:space="preserve"><value>Product-URL</value></data>
|
||||
<data name="RegistryAdmin.AutoFetch" xml:space="preserve"><value>Automatisch ophalen</value></data>
|
||||
<data name="RegistryAdmin.PictureUrl" xml:space="preserve"><value>Afbeeldings-URL</value></data>
|
||||
<data name="RegistryAdmin.Description" xml:space="preserve"><value>Beschrijving</value></data>
|
||||
<data name="RegistryAdmin.Price" xml:space="preserve"><value>Prijs</value></data>
|
||||
<data name="RegistryAdmin.CurrencySymbol" xml:space="preserve"><value>Valutasymbool</value></data>
|
||||
<data name="RegistryAdmin.DesiredQty" xml:space="preserve"><value>Gewenst aantal</value></data>
|
||||
<data name="RegistryAdmin.Participation" xml:space="preserve"><value>Deelname</value></data>
|
||||
<data name="RegistryAdmin.ParticipationTarget" xml:space="preserve"><value>Doelbedrag deelname</value></data>
|
||||
<data name="RegistryAdmin.SecondHandPreference" xml:space="preserve"><value>Voorkeur tweedehands</value></data>
|
||||
<data name="RegistryAdmin.SecondHandOptional" xml:space="preserve"><value>Tweedehands mogelijk</value></data>
|
||||
<data name="RegistryAdmin.PreferSecondHand" xml:space="preserve"><value>Tweedehands verkiezen</value></data>
|
||||
<data name="RegistryAdmin.NewOnly" xml:space="preserve"><value>Enkel nieuw</value></data>
|
||||
<data name="RegistryAdmin.Given" xml:space="preserve"><value>Gegeven</value></data>
|
||||
<data name="RegistryAdmin.Category" xml:space="preserve"><value>Categorie</value></data>
|
||||
<data name="RegistryAdmin.SaveItem" xml:space="preserve"><value>Item opslaan</value></data>
|
||||
<data name="RegistryAdmin.CategoriesAndItems" xml:space="preserve"><value>Categorieën en items</value></data>
|
||||
<data name="RegistryAdmin.NewCategory" xml:space="preserve"><value>Nieuwe categorie</value></data>
|
||||
<data name="RegistryAdmin.AddCategory" xml:space="preserve"><value>Categorie toevoegen</value></data>
|
||||
<data name="RegistryAdmin.DragHint" xml:space="preserve"><value>Sleep categorieën of items om te herschikken. Zet items in een andere categorie om ze te groeperen.</value></data>
|
||||
<data name="RegistryAdmin.Rename" xml:space="preserve"><value>Hernoemen</value></data>
|
||||
<data name="RegistryAdmin.DropItemsHere" xml:space="preserve"><value>Sleep items hierheen.</value></data>
|
||||
<data name="RegistryAdmin.Header.Name" xml:space="preserve"><value>Naam</value></data>
|
||||
<data name="RegistryAdmin.Header.DesiredQty" xml:space="preserve"><value>Gewenst aantal</value></data>
|
||||
<data name="RegistryAdmin.Header.Condition" xml:space="preserve"><value>Staat</value></data>
|
||||
<data name="RegistryAdmin.Header.Participation" xml:space="preserve"><value>Deelname</value></data>
|
||||
<data name="RegistryAdmin.Header.PurchasedContributedBy" xml:space="preserve"><value>Gekocht door / Bijgedragen door</value></data>
|
||||
<data name="RegistryAdmin.Purchased" xml:space="preserve"><value>Gekocht:</value></data>
|
||||
<data name="RegistryAdmin.Contributed" xml:space="preserve"><value>Bijgedragen:</value></data>
|
||||
<data name="RegistryAdmin.RegistrySettings" xml:space="preserve"><value>Lijstinstellingen</value></data>
|
||||
<data name="RegistryAdmin.BabyName" xml:space="preserve"><value>Naam baby</value></data>
|
||||
<data name="RegistryAdmin.BirthDate" xml:space="preserve"><value>Geboortedatum</value></data>
|
||||
<data name="RegistryAdmin.Theme" xml:space="preserve"><value>Thema</value></data>
|
||||
<data name="RegistryAdmin.Theme.Default" xml:space="preserve"><value>Standaard</value></data>
|
||||
<data name="RegistryAdmin.Theme.Soft" xml:space="preserve"><value>Zacht</value></data>
|
||||
<data name="RegistryAdmin.Theme.Modern" xml:space="preserve"><value>Modern</value></data>
|
||||
<data name="RegistryAdmin.ShippingAddress" xml:space="preserve"><value>Leveringsadres</value></data>
|
||||
<data name="RegistryAdmin.LineBreaksPreserved" xml:space="preserve"><value>Regeleinden blijven behouden</value></data>
|
||||
<data name="RegistryAdmin.HeroImage" xml:space="preserve"><value>Hero-afbeelding</value></data>
|
||||
<data name="RegistryAdmin.HideHeaderName" xml:space="preserve"><value>Naamkop op openbare pagina verbergen</value></data>
|
||||
<data name="RegistryAdmin.UploadImage" xml:space="preserve"><value>Afbeelding uploaden</value></data>
|
||||
<data name="RegistryAdmin.RemoveHeroImage" xml:space="preserve"><value>Hero-afbeelding verwijderen</value></data>
|
||||
<data name="RegistryAdmin.CurrentHeroImage" xml:space="preserve"><value>Huidige hero-afbeelding</value></data>
|
||||
<data name="RegistryAdmin.TopContent" xml:space="preserve"><value>Bovenste inhoud</value></data>
|
||||
<data name="RegistryAdmin.WelcomeText" xml:space="preserve"><value>Welkomsttekst</value></data>
|
||||
<data name="RegistryAdmin.BankAccountSettings" xml:space="preserve"><value>Bankrekeninginstellingen</value></data>
|
||||
<data name="RegistryAdmin.BankAccountName" xml:space="preserve"><value>Naam bankrekening</value></data>
|
||||
<data name="RegistryAdmin.DisplayBankAccountName" xml:space="preserve"><value>Naam bankrekening tonen</value></data>
|
||||
<data name="RegistryAdmin.ContributionPaymentOptions" xml:space="preserve"><value>Betaalopties voor deelname</value></data>
|
||||
<data name="RegistryAdmin.SingleQrCodeUrl" xml:space="preserve"><value>URL van enkele QR-code</value></data>
|
||||
<data name="RegistryAdmin.SingleQrHelp" xml:space="preserve"><value>Optioneel: één QR-code die schenkers voor eender welk bedrag kunnen scannen.</value></data>
|
||||
<data name="RegistryAdmin.AmountSpecificQrCodes" xml:space="preserve"><value>Bedragsspecifieke QR-codes</value></data>
|
||||
<data name="RegistryAdmin.AddQrAmount" xml:space="preserve"><value>QR-bedrag toevoegen</value></data>
|
||||
<data name="RegistryAdmin.NoAmountSpecificQrCodes" xml:space="preserve"><value>Geen bedragsspecifieke QR-codes ingesteld.</value></data>
|
||||
<data name="RegistryAdmin.QrCodeUrl" xml:space="preserve"><value>QR-code-URL</value></data>
|
||||
<data name="RegistryAdmin.SaveSettings" xml:space="preserve"><value>Instellingen opslaan</value></data>
|
||||
<data name="RegistryAdmin.UserAddresses" xml:space="preserve"><value>Gebruikersadressen</value></data>
|
||||
<data name="RegistryAdmin.NoUsersFound" xml:space="preserve"><value>Nog geen gebruikers gevonden.</value></data>
|
||||
<data name="RegistryAdmin.Email" xml:space="preserve"><value>E-mail</value></data>
|
||||
<data name="RegistryAdmin.Address" xml:space="preserve"><value>Adres</value></data>
|
||||
<data name="RegistryAdmin.CurrentAdministrators" xml:space="preserve"><value>Huidige beheerders</value></data>
|
||||
<data name="RegistryAdmin.NoAdmins" xml:space="preserve"><value>Nog geen beheerders toegewezen.</value></data>
|
||||
<data name="RegistryAdmin.EmailOrName" xml:space="preserve"><value>E-mail / Naam</value></data>
|
||||
<data name="RegistryAdmin.InviteAdministrator" xml:space="preserve"><value>Beheerder uitnodigen</value></data>
|
||||
<data name="RegistryAdmin.OptionalEmail" xml:space="preserve"><value>optionele e-mail</value></data>
|
||||
<data name="RegistryAdmin.CreateInvite" xml:space="preserve"><value>Uitnodiging aanmaken</value></data>
|
||||
<data name="RegistryAdmin.InviteLink" xml:space="preserve"><value>Uitnodigingslink:</value></data>
|
||||
|
||||
<data name="RegistryActionLog.PageTitle" xml:space="preserve"><value>Actielog - Lijstbeheer</value></data>
|
||||
<data name="RegistryActionLog.Title" xml:space="preserve"><value>Actielog van de lijst</value></data>
|
||||
<data name="RegistryActionLog.Description" xml:space="preserve"><value>Dit log toont alle gebruikersacties op deze lijst: aankopen, deelnames en andere interacties.</value></data>
|
||||
<data name="RegistryActionLog.NoActions" xml:space="preserve"><value>Nog geen acties geregistreerd.</value></data>
|
||||
<data name="RegistryActionLog.DateTime" xml:space="preserve"><value>Datum/tijd</value></data>
|
||||
<data name="RegistryActionLog.User" xml:space="preserve"><value>Gebruiker</value></data>
|
||||
<data name="RegistryActionLog.Action" xml:space="preserve"><value>Actie</value></data>
|
||||
<data name="RegistryActionLog.Item" xml:space="preserve"><value>Item</value></data>
|
||||
<data name="RegistryActionLog.Quantity" xml:space="preserve"><value>Aantal</value></data>
|
||||
<data name="RegistryActionLog.Amount" xml:space="preserve"><value>Bedrag</value></data>
|
||||
<data name="RegistryActionLog.Details" xml:space="preserve"><value>Details</value></data>
|
||||
<data name="RegistryActionLog.Badge.RegistryOpened" xml:space="preserve"><value>Lijst geopend</value></data>
|
||||
<data name="RegistryActionLog.Badge.ItemLinkOpened" xml:space="preserve"><value>Itemlink geopend</value></data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseMarked" xml:space="preserve"><value>Aankoop gemarkeerd</value></data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve"><value>Aankoop verwijderd</value></data>
|
||||
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve"><value>Gedeeltelijke aankoop</value></data>
|
||||
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve"><value>Deelname geregistreerd</value></data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve"><value>Automatisch ophalen gelukt</value></data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve"><value>Automatisch ophalen mislukt</value></data>
|
||||
|
||||
<data name="Error.PageTitle" xml:space="preserve"><value>Fout</value></data>
|
||||
<data name="Error.Title" xml:space="preserve"><value>Fout.</value></data>
|
||||
<data name="Error.Subtitle" xml:space="preserve"><value>Er is een fout opgetreden bij het verwerken van je aanvraag.</value></data>
|
||||
<data name="Error.RequestId" xml:space="preserve"><value>Aanvraag-ID:</value></data>
|
||||
<data name="Error.DevMode" xml:space="preserve"><value>Ontwikkelmodus</value></data>
|
||||
<data name="Error.DevHint1" xml:space="preserve"><value>Wanneer je naar de Development-omgeving schakelt, zie je meer gedetailleerde foutinformatie.</value></data>
|
||||
<data name="Error.DevHint2" xml:space="preserve"><value>De Development-omgeving mag niet ingeschakeld zijn op gedeployde toepassingen.</value></data>
|
||||
<data name="Error.DevHint3" xml:space="preserve"><value>Dat kan gevoelige informatie uit uitzonderingen tonen aan eindgebruikers.</value></data>
|
||||
<data name="Error.DevHint4" xml:space="preserve"><value>Voor lokaal debuggen: zet ASPNETCORE_ENVIRONMENT op Development en herstart de app.</value></data>
|
||||
|
||||
<data name="MainLayout.UnhandledError" xml:space="preserve"><value>Er is een onverwachte fout opgetreden.</value></data>
|
||||
<data name="MainLayout.Reload" xml:space="preserve"><value>Herladen</value></data>
|
||||
|
||||
<data name="NavMenu.Brand" xml:space="preserve"><value>BirthList</value></data>
|
||||
<data name="NavMenu.NavigationMenu" xml:space="preserve"><value>Navigatiemenu</value></data>
|
||||
<data name="NavMenu.Home" xml:space="preserve"><value>Start</value></data>
|
||||
<data name="NavMenu.Logout" xml:space="preserve"><value>Afmelden</value></data>
|
||||
<data name="NavMenu.Register" xml:space="preserve"><value>Registreren</value></data>
|
||||
<data name="NavMenu.Login" xml:space="preserve"><value>Aanmelden</value></data>
|
||||
</root>
|
||||
@@ -0,0 +1,705 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
|
||||
</resheader>
|
||||
<data name="Common.Yes" xml:space="preserve">
|
||||
<value>[[Yes]]</value>
|
||||
</data>
|
||||
<data name="Common.No" xml:space="preserve">
|
||||
<value>[[No]]</value>
|
||||
</data>
|
||||
<data name="Common.Save" xml:space="preserve">
|
||||
<value>[[Save]]</value>
|
||||
</data>
|
||||
<data name="Common.Cancel" xml:space="preserve">
|
||||
<value>[[Cancel]]</value>
|
||||
</data>
|
||||
<data name="Common.Remove" xml:space="preserve">
|
||||
<value>[[Remove]]</value>
|
||||
</data>
|
||||
<data name="Common.Edit" xml:space="preserve">
|
||||
<value>[[Edit]]</value>
|
||||
</data>
|
||||
<data name="Common.Back" xml:space="preserve">
|
||||
<value>[[Back]]</value>
|
||||
</data>
|
||||
<data name="Common.Next" xml:space="preserve">
|
||||
<value>[[Next]]</value>
|
||||
</data>
|
||||
<data name="Common.Loading" xml:space="preserve">
|
||||
<value>[[Loading...]]</value>
|
||||
</data>
|
||||
<data name="Common.AccessDenied" xml:space="preserve">
|
||||
<value>[[Access denied.]]</value>
|
||||
</data>
|
||||
<data name="Common.SelectUser" xml:space="preserve">
|
||||
<value>[[Select user]]</value>
|
||||
</data>
|
||||
<data name="Common.SearchUser" xml:space="preserve">
|
||||
<value>[[Search user]]</value>
|
||||
</data>
|
||||
<data name="Common.Quantity" xml:space="preserve">
|
||||
<value>[[Quantity]]</value>
|
||||
</data>
|
||||
<data name="Common.Amount" xml:space="preserve">
|
||||
<value>[[Amount]]</value>
|
||||
</data>
|
||||
<data name="Common.Message" xml:space="preserve">
|
||||
<value>[[Message]]</value>
|
||||
</data>
|
||||
<data name="TopBar.Language" xml:space="preserve">
|
||||
<value>[[Language]]</value>
|
||||
</data>
|
||||
<data name="TopBar.Brand" xml:space="preserve">
|
||||
<value>[[Gift List]]</value>
|
||||
</data>
|
||||
<data name="TopBar.ProfilePrompt" xml:space="preserve">
|
||||
<value>[[Please complete your profile (first name, last name, and address).]]</value>
|
||||
</data>
|
||||
<data name="TopBar.CompleteProfile" xml:space="preserve">
|
||||
<value>[[Complete profile]]</value>
|
||||
</data>
|
||||
<data name="TopBar.AccountSettings" xml:space="preserve">
|
||||
<value>[[Account settings]]</value>
|
||||
</data>
|
||||
<data name="TopBar.SignIn" xml:space="preserve">
|
||||
<value>[[Sign in]]</value>
|
||||
</data>
|
||||
<data name="TopBar.SignOut" xml:space="preserve">
|
||||
<value>[[Sign out]]</value>
|
||||
</data>
|
||||
<data name="Home.PageTitle" xml:space="preserve">
|
||||
<value>[[Birth Registry]]</value>
|
||||
</data>
|
||||
<data name="Home.Welcome" xml:space="preserve">
|
||||
<value>[[Welcome to Gift List]]</value>
|
||||
</data>
|
||||
<data name="Home.ManagedRegistries" xml:space="preserve">
|
||||
<value>[[Registries you manage]]</value>
|
||||
</data>
|
||||
<data name="Home.CreateNew" xml:space="preserve">
|
||||
<value>[[Create new]]</value>
|
||||
</data>
|
||||
<data name="Home.NoRegistries" xml:space="preserve">
|
||||
<value>[[No registries yet.]]</value>
|
||||
</data>
|
||||
<data name="Home.View" xml:space="preserve">
|
||||
<value>[[View]]</value>
|
||||
</data>
|
||||
<data name="Home.Manage" xml:space="preserve">
|
||||
<value>[[Manage]]</value>
|
||||
</data>
|
||||
<data name="Home.VisitedRegistries" xml:space="preserve">
|
||||
<value>[[Visited registries]]</value>
|
||||
</data>
|
||||
<data name="Home.NoVisitedRegistries" xml:space="preserve">
|
||||
<value>[[No visited registries yet.]]</value>
|
||||
</data>
|
||||
<data name="Home.CreateRegistryTitle" xml:space="preserve">
|
||||
<value>[[Create new registry]]</value>
|
||||
</data>
|
||||
<data name="Home.Title" xml:space="preserve">
|
||||
<value>[[Title]]</value>
|
||||
</data>
|
||||
<data name="Home.Type" xml:space="preserve">
|
||||
<value>[[Type]]</value>
|
||||
</data>
|
||||
<data name="Home.Theme" xml:space="preserve">
|
||||
<value>[[Theme]]</value>
|
||||
</data>
|
||||
<data name="Home.Theme.Default" xml:space="preserve">
|
||||
<value>[[Default]]</value>
|
||||
</data>
|
||||
<data name="Home.Theme.Soft" xml:space="preserve">
|
||||
<value>[[Soft]]</value>
|
||||
</data>
|
||||
<data name="Home.Theme.Modern" xml:space="preserve">
|
||||
<value>[[Modern]]</value>
|
||||
</data>
|
||||
<data name="Home.RegistryType.Birth" xml:space="preserve">
|
||||
<value>[[Birth]]</value>
|
||||
</data>
|
||||
<data name="Home.RegistryType.Wedding" xml:space="preserve">
|
||||
<value>[[Wedding]]</value>
|
||||
</data>
|
||||
<data name="Home.RegistryType.Birthday" xml:space="preserve">
|
||||
<value>[[Birthday]]</value>
|
||||
</data>
|
||||
<data name="Home.MustBeLoggedIn" xml:space="preserve">
|
||||
<value>[[You must be logged in to create a registry.]]</value>
|
||||
</data>
|
||||
<data name="Home.TitleRequired" xml:space="preserve">
|
||||
<value>[[Title is required.]]</value>
|
||||
</data>
|
||||
<data name="Home.LoginPromptPrefix" xml:space="preserve">
|
||||
<value>[[Please ]]</value>
|
||||
</data>
|
||||
<data name="Home.LoginPromptLinkText" xml:space="preserve">
|
||||
<value>[[log in]]</value>
|
||||
</data>
|
||||
<data name="Home.LoginPromptSuffix" xml:space="preserve">
|
||||
<value>[[ to create and manage registries.]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.PageTitle" xml:space="preserve">
|
||||
<value>[[Registry]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.NotFound" xml:space="preserve">
|
||||
<value>[[Registry not found.]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.GoToAdmin" xml:space="preserve">
|
||||
<value>[[Go to Admin]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ShippingAddress" xml:space="preserve">
|
||||
<value>[[Shipping address]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.BankTransferParticipation" xml:space="preserve">
|
||||
<value>[[Bank transfer participation]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.IBAN" xml:space="preserve">
|
||||
<value>[[IBAN]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.BIC" xml:space="preserve">
|
||||
<value>[[BIC]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.SecondHandPreferred" xml:space="preserve">
|
||||
<value>[[Second-hand preferred]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.SecondHandOptional" xml:space="preserve">
|
||||
<value>[[Second-hand optional]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Qty" xml:space="preserve">
|
||||
<value>[[Qty:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.PurchasedSuffix" xml:space="preserve">
|
||||
<value>[[purchased]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Price" xml:space="preserve">
|
||||
<value>[[Price:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Participation" xml:space="preserve">
|
||||
<value>[[Participation:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.OutOfFulfilled" xml:space="preserve">
|
||||
<value>[[out of {0}{1} fulfilled]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.FulfilledOnly" xml:space="preserve">
|
||||
<value>[[fulfilled]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.PurchasedBy" xml:space="preserve">
|
||||
<value>[[Purchased by:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ContributedBy" xml:space="preserve">
|
||||
<value>[[Contributed by:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.YouQuantity" xml:space="preserve">
|
||||
<value>[[You ({0})]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.AndOtherPeople" xml:space="preserve">
|
||||
<value>[[and {0} other {1}]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Person" xml:space="preserve">
|
||||
<value>[[person]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.People" xml:space="preserve">
|
||||
<value>[[people]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.AndOthers" xml:space="preserve">
|
||||
<value>[[and others]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Purchased" xml:space="preserve">
|
||||
<value>[[Purchased]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Contributed" xml:space="preserve">
|
||||
<value>[[Contributed]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.LoginToPurchase" xml:space="preserve">
|
||||
<value>[[Login to purchase]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Purchase" xml:space="preserve">
|
||||
<value>[[Purchase]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ManagePurchases" xml:space="preserve">
|
||||
<value>[[Manage purchases]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.EditPurchase" xml:space="preserve">
|
||||
<value>[[Edit purchase]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.MarkPurchased" xml:space="preserve">
|
||||
<value>[[Mark purchased]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ManageParticipations" xml:space="preserve">
|
||||
<value>[[Manage participations]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.EditParticipation" xml:space="preserve">
|
||||
<value>[[Edit participation]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.PartiallyFulfill" xml:space="preserve">
|
||||
<value>[[Partially fulfill]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.MarkAsPurchased" xml:space="preserve">
|
||||
<value>[[Mark as purchased]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.HowManyUnitsPurchased" xml:space="preserve">
|
||||
<value>[[How many units did you purchase?]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.OpenProductLink" xml:space="preserve">
|
||||
<value>[[Open product link]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ConfirmPurchase" xml:space="preserve">
|
||||
<value>[[Confirm purchase]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.LogContribution" xml:space="preserve">
|
||||
<value>[[Log contribution]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.TransferredAmount" xml:space="preserve">
|
||||
<value>[[Transferred amount]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Confirm" xml:space="preserve">
|
||||
<value>[[Confirm]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.SelectPurchaserToUnmark" xml:space="preserve">
|
||||
<value>[[Select purchaser to unmark]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.MultipleUsersPurchased" xml:space="preserve">
|
||||
<value>[[Multiple users have purchased this item. Choose which purchase to unmark:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.Unmark" xml:space="preserve">
|
||||
<value>[[Unmark]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.UnmarkAll" xml:space="preserve">
|
||||
<value>[[Unmark all]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.PartiallyFulfillItem" xml:space="preserve">
|
||||
<value>[[Partially fulfill item]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.SelectDonateMethod" xml:space="preserve">
|
||||
<value>[[Select how you want to donate:]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.IbanTransfer" xml:space="preserve">
|
||||
<value>[[IBAN transfer]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.SingleQrCode" xml:space="preserve">
|
||||
<value>[[Single QR code]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.QrCodePerAmount" xml:space="preserve">
|
||||
<value>[[QR code per amount]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.OpenPaymentLink" xml:space="preserve">
|
||||
<value>[[Open payment link]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.HowMuchAdded" xml:space="preserve">
|
||||
<value>[[How much did you add for this item?]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.NoQrForAmount" xml:space="preserve">
|
||||
<value>[[No QR code configured for this exact amount.]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ManagePurchase" xml:space="preserve">
|
||||
<value>[[Manage purchase]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.ManageParticipation" xml:space="preserve">
|
||||
<value>[[Manage participation]]</value>
|
||||
</data>
|
||||
<data name="RegistryPublic.User" xml:space="preserve">
|
||||
<value>[[User]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.PageTitle" xml:space="preserve">
|
||||
<value>[[Contribution Amount]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.ItemNotFound" xml:space="preserve">
|
||||
<value>[[Item not found.]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.LoginFirst" xml:space="preserve">
|
||||
<value>[[Please log in first.]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.PartiallyFulfill" xml:space="preserve">
|
||||
<value>[[Partially fulfill: {0}]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.NoRepresentableAmount" xml:space="preserve">
|
||||
<value>[[No representable amount can be formed from configured QR codes up to {0}200.]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.SelectAmount" xml:space="preserve">
|
||||
<value>[[Select amount: {0}{1}]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.NoQrCombination" xml:space="preserve">
|
||||
<value>[[No QR combination available for this amount.]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.SuggestedCombination" xml:space="preserve">
|
||||
<value>[[Suggested QR combination:]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.OpenPaymentLink" xml:space="preserve">
|
||||
<value>[[Open payment link]]</value>
|
||||
</data>
|
||||
<data name="RegistryContributionAmount.TransferredAmount" xml:space="preserve">
|
||||
<value>[[I transferred this amount]]</value>
|
||||
</data>
|
||||
<data name="RegistryInvite.PageTitle" xml:space="preserve">
|
||||
<value>[[Admin Invite]]</value>
|
||||
</data>
|
||||
<data name="RegistryInvite.Title" xml:space="preserve">
|
||||
<value>[[Admin invitation]]</value>
|
||||
</data>
|
||||
<data name="RegistryInvite.Validating" xml:space="preserve">
|
||||
<value>[[Validating invitation...]]</value>
|
||||
</data>
|
||||
<data name="RegistryInvite.Accepted" xml:space="preserve">
|
||||
<value>[[Invitation accepted. You are now an admin.]]</value>
|
||||
</data>
|
||||
<data name="RegistryInvite.GoToAdmin" xml:space="preserve">
|
||||
<value>[[Go to admin]]</value>
|
||||
</data>
|
||||
<data name="RegistryInvite.Invalid" xml:space="preserve">
|
||||
<value>[[The invitation is invalid or already used.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.PageTitle" xml:space="preserve">
|
||||
<value>[[Registry Admin]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Title" xml:space="preserve">
|
||||
<value>[[Registry Admin]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SmtpNotConfigured" xml:space="preserve">
|
||||
<value>[[SMTP is not configured. Email features (identity emails and admin invite emails) are disabled. Configure the Smtp section in appsettings or user secrets.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Tab.Items" xml:space="preserve">
|
||||
<value>[[Items]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Tab.Settings" xml:space="preserve">
|
||||
<value>[[Settings]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Tab.Administrators" xml:space="preserve">
|
||||
<value>[[Administrators]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Tab.Addresses" xml:space="preserve">
|
||||
<value>[[Addresses]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Tab.ActionLog" xml:space="preserve">
|
||||
<value>[[Action Log]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.ViewPublicList" xml:space="preserve">
|
||||
<value>[[View Public List]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.AddOrEditItem" xml:space="preserve">
|
||||
<value>[[Add or edit item]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Name" xml:space="preserve">
|
||||
<value>[[Name]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.ProductUrl" xml:space="preserve">
|
||||
<value>[[Product URL]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.AutoFetch" xml:space="preserve">
|
||||
<value>[[Auto fetch]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.PictureUrl" xml:space="preserve">
|
||||
<value>[[Picture URL]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Description" xml:space="preserve">
|
||||
<value>[[Description]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Price" xml:space="preserve">
|
||||
<value>[[Price]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.CurrencySymbol" xml:space="preserve">
|
||||
<value>[[Currency symbol]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.DesiredQty" xml:space="preserve">
|
||||
<value>[[Desired qty]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Participation" xml:space="preserve">
|
||||
<value>[[Participation]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.ParticipationTarget" xml:space="preserve">
|
||||
<value>[[Participation target]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SecondHandPreference" xml:space="preserve">
|
||||
<value>[[Second hand preference]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SecondHandOptional" xml:space="preserve">
|
||||
<value>[[Second hand optional]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.PreferSecondHand" xml:space="preserve">
|
||||
<value>[[Prefer second hand]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.NewOnly" xml:space="preserve">
|
||||
<value>[[New only]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Given" xml:space="preserve">
|
||||
<value>[[Given]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Category" xml:space="preserve">
|
||||
<value>[[Category]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SaveItem" xml:space="preserve">
|
||||
<value>[[Save item]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.CategoriesAndItems" xml:space="preserve">
|
||||
<value>[[Categories and items]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.NewCategory" xml:space="preserve">
|
||||
<value>[[New category]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.AddCategory" xml:space="preserve">
|
||||
<value>[[Add category]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.DragHint" xml:space="preserve">
|
||||
<value>[[Drag categories or items to reorder. Drop items into another category to regroup them.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Rename" xml:space="preserve">
|
||||
<value>[[Rename]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.DropItemsHere" xml:space="preserve">
|
||||
<value>[[Drop items here.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Header.Name" xml:space="preserve">
|
||||
<value>[[Name]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Header.DesiredQty" xml:space="preserve">
|
||||
<value>[[Desired Qty]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Header.Condition" xml:space="preserve">
|
||||
<value>[[Condition]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Header.Participation" xml:space="preserve">
|
||||
<value>[[Participation]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Header.PurchasedContributedBy" xml:space="preserve">
|
||||
<value>[[Purchased by / Contributed by]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Purchased" xml:space="preserve">
|
||||
<value>[[Purchased:]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Contributed" xml:space="preserve">
|
||||
<value>[[Contributed:]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.RegistrySettings" xml:space="preserve">
|
||||
<value>[[Registry Settings]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.BabyName" xml:space="preserve">
|
||||
<value>[[Baby name]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.BirthDate" xml:space="preserve">
|
||||
<value>[[Birth date]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Theme" xml:space="preserve">
|
||||
<value>[[Theme]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Theme.Default" xml:space="preserve">
|
||||
<value>[[Default]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Theme.Soft" xml:space="preserve">
|
||||
<value>[[Soft]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Theme.Modern" xml:space="preserve">
|
||||
<value>[[Modern]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.ShippingAddress" xml:space="preserve">
|
||||
<value>[[Shipping address]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.LineBreaksPreserved" xml:space="preserve">
|
||||
<value>[[Line breaks will be preserved]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.HeroImage" xml:space="preserve">
|
||||
<value>[[Hero image]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.HideHeaderName" xml:space="preserve">
|
||||
<value>[[Hide name heading on public page]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.UploadImage" xml:space="preserve">
|
||||
<value>[[Upload image]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.RemoveHeroImage" xml:space="preserve">
|
||||
<value>[[Remove hero image]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.CurrentHeroImage" xml:space="preserve">
|
||||
<value>[[Current hero image]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.TopContent" xml:space="preserve">
|
||||
<value>[[Top content]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.WelcomeText" xml:space="preserve">
|
||||
<value>[[Welcome text]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.BankAccountSettings" xml:space="preserve">
|
||||
<value>[[Bank Account Settings]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.BankAccountName" xml:space="preserve">
|
||||
<value>[[Bank account name]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.DisplayBankAccountName" xml:space="preserve">
|
||||
<value>[[Display bank account name]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.ContributionPaymentOptions" xml:space="preserve">
|
||||
<value>[[Contribution Payment Options]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SingleQrCodeUrl" xml:space="preserve">
|
||||
<value>[[Single QR code URL]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SingleQrHelp" xml:space="preserve">
|
||||
<value>[[Optional: one QR code that donors can scan for any amount.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.AmountSpecificQrCodes" xml:space="preserve">
|
||||
<value>[[Amount-specific QR codes]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.AddQrAmount" xml:space="preserve">
|
||||
<value>[[Add QR amount]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.NoAmountSpecificQrCodes" xml:space="preserve">
|
||||
<value>[[No amount-specific QR codes configured.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.QrCodeUrl" xml:space="preserve">
|
||||
<value>[[QR code URL]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.SaveSettings" xml:space="preserve">
|
||||
<value>[[Save settings]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.UserAddresses" xml:space="preserve">
|
||||
<value>[[User addresses]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.NoUsersFound" xml:space="preserve">
|
||||
<value>[[No users found yet.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Email" xml:space="preserve">
|
||||
<value>[[Email]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.Address" xml:space="preserve">
|
||||
<value>[[Address]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.CurrentAdministrators" xml:space="preserve">
|
||||
<value>[[Current administrators]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.NoAdmins" xml:space="preserve">
|
||||
<value>[[No admins assigned yet.]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.EmailOrName" xml:space="preserve">
|
||||
<value>[[Email / Name]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.InviteAdministrator" xml:space="preserve">
|
||||
<value>[[Invite administrator]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.OptionalEmail" xml:space="preserve">
|
||||
<value>[[optional email]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.CreateInvite" xml:space="preserve">
|
||||
<value>[[Create invite]]</value>
|
||||
</data>
|
||||
<data name="RegistryAdmin.InviteLink" xml:space="preserve">
|
||||
<value>[[Invite link:]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.PageTitle" xml:space="preserve">
|
||||
<value>[[Action Log - Registry Admin]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Title" xml:space="preserve">
|
||||
<value>[[Registry Action Log]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Description" xml:space="preserve">
|
||||
<value>[[This log shows all user actions on this registry: purchases, contributions, and other interactions.]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.NoActions" xml:space="preserve">
|
||||
<value>[[No actions recorded yet.]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.DateTime" xml:space="preserve">
|
||||
<value>[[Date/Time]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.User" xml:space="preserve">
|
||||
<value>[[User]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Action" xml:space="preserve">
|
||||
<value>[[Action]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Item" xml:space="preserve">
|
||||
<value>[[Item]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Quantity" xml:space="preserve">
|
||||
<value>[[Quantity]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Amount" xml:space="preserve">
|
||||
<value>[[Amount]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Details" xml:space="preserve">
|
||||
<value>[[Details]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.RegistryOpened" xml:space="preserve">
|
||||
<value>[[Registry opened]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.ItemLinkOpened" xml:space="preserve">
|
||||
<value>[[Item link opened]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseMarked" xml:space="preserve">
|
||||
<value>[[Purchase marked]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve">
|
||||
<value>[[Purchase unmarked]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve">
|
||||
<value>[[Partial purchase]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve">
|
||||
<value>[[Contribution logged]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve">
|
||||
<value>[[Auto fetch succeeded]]</value>
|
||||
</data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve">
|
||||
<value>[[Auto fetch failed]]</value>
|
||||
</data>
|
||||
<data name="Error.PageTitle" xml:space="preserve">
|
||||
<value>[[Error]]</value>
|
||||
</data>
|
||||
<data name="Error.Title" xml:space="preserve">
|
||||
<value>[[Error.]]</value>
|
||||
</data>
|
||||
<data name="Error.Subtitle" xml:space="preserve">
|
||||
<value>[[An error occurred while processing your request.]]</value>
|
||||
</data>
|
||||
<data name="Error.RequestId" xml:space="preserve">
|
||||
<value>[[Request ID:]]</value>
|
||||
</data>
|
||||
<data name="Error.DevMode" xml:space="preserve">
|
||||
<value>[[Development Mode]]</value>
|
||||
</data>
|
||||
<data name="Error.DevHint1" xml:space="preserve">
|
||||
<value>[[Swapping to Development environment will display more detailed information about the error that occurred.]]</value>
|
||||
</data>
|
||||
<data name="Error.DevHint2" xml:space="preserve">
|
||||
<value>[[The Development environment shouldn't be enabled for deployed applications.]]</value>
|
||||
</data>
|
||||
<data name="Error.DevHint3" xml:space="preserve">
|
||||
<value>[[It can result in displaying sensitive information from exceptions to end users.]]</value>
|
||||
</data>
|
||||
<data name="Error.DevHint4" xml:space="preserve">
|
||||
<value>[[For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.]]</value>
|
||||
</data>
|
||||
<data name="MainLayout.UnhandledError" xml:space="preserve">
|
||||
<value>[[An unhandled error has occurred.]]</value>
|
||||
</data>
|
||||
<data name="MainLayout.Reload" xml:space="preserve">
|
||||
<value>[[Reload]]</value>
|
||||
</data>
|
||||
<data name="NavMenu.Brand" xml:space="preserve">
|
||||
<value>[[BirthList]]</value>
|
||||
</data>
|
||||
<data name="NavMenu.NavigationMenu" xml:space="preserve">
|
||||
<value>[[Navigation menu]]</value>
|
||||
</data>
|
||||
<data name="NavMenu.Home" xml:space="preserve">
|
||||
<value>[[Home]]</value>
|
||||
</data>
|
||||
<data name="NavMenu.Logout" xml:space="preserve">
|
||||
<value>[[Logout]]</value>
|
||||
</data>
|
||||
<data name="NavMenu.Register" xml:space="preserve">
|
||||
<value>[[Register]]</value>
|
||||
</data>
|
||||
<data name="NavMenu.Login" xml:space="preserve">
|
||||
<value>[[Login]]</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, ...</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, ...</value>
|
||||
</resheader>
|
||||
|
||||
<data name="Common.Yes" xml:space="preserve"><value>Yes</value></data>
|
||||
<data name="Common.No" xml:space="preserve"><value>No</value></data>
|
||||
<data name="Common.Save" xml:space="preserve"><value>Save</value></data>
|
||||
<data name="Common.Cancel" xml:space="preserve"><value>Cancel</value></data>
|
||||
<data name="Common.Remove" xml:space="preserve"><value>Remove</value></data>
|
||||
<data name="Common.Edit" xml:space="preserve"><value>Edit</value></data>
|
||||
<data name="Common.Back" xml:space="preserve"><value>Back</value></data>
|
||||
<data name="Common.Next" xml:space="preserve"><value>Next</value></data>
|
||||
<data name="Common.Loading" xml:space="preserve"><value>Loading...</value></data>
|
||||
<data name="Common.AccessDenied" xml:space="preserve"><value>Access denied.</value></data>
|
||||
<data name="Common.SelectUser" xml:space="preserve"><value>Select user</value></data>
|
||||
<data name="Common.SearchUser" xml:space="preserve"><value>Search user</value></data>
|
||||
<data name="Common.Quantity" xml:space="preserve"><value>Quantity</value></data>
|
||||
<data name="Common.Amount" xml:space="preserve"><value>Amount</value></data>
|
||||
<data name="Common.Message" xml:space="preserve"><value>Message</value></data>
|
||||
|
||||
<data name="TopBar.Language" xml:space="preserve"><value>Language</value></data>
|
||||
<data name="TopBar.Brand" xml:space="preserve"><value>Gift List</value></data>
|
||||
<data name="TopBar.ProfilePrompt" xml:space="preserve"><value>Please complete your profile (first name, last name, and address).</value></data>
|
||||
<data name="TopBar.CompleteProfile" xml:space="preserve"><value>Complete profile</value></data>
|
||||
<data name="TopBar.AccountSettings" xml:space="preserve"><value>Account settings</value></data>
|
||||
<data name="TopBar.SignIn" xml:space="preserve"><value>Sign in</value></data>
|
||||
<data name="TopBar.SignOut" xml:space="preserve"><value>Sign out</value></data>
|
||||
|
||||
<data name="Home.PageTitle" xml:space="preserve"><value>Birth Registry</value></data>
|
||||
<data name="Home.Welcome" xml:space="preserve"><value>Welcome to Gift List</value></data>
|
||||
<data name="Home.ManagedRegistries" xml:space="preserve"><value>Registries you manage</value></data>
|
||||
<data name="Home.CreateNew" xml:space="preserve"><value>Create new</value></data>
|
||||
<data name="Home.NoRegistries" xml:space="preserve"><value>No registries yet.</value></data>
|
||||
<data name="Home.View" xml:space="preserve"><value>View</value></data>
|
||||
<data name="Home.Manage" xml:space="preserve"><value>Manage</value></data>
|
||||
<data name="Home.VisitedRegistries" xml:space="preserve"><value>Visited registries</value></data>
|
||||
<data name="Home.NoVisitedRegistries" xml:space="preserve"><value>No visited registries yet.</value></data>
|
||||
<data name="Home.CreateRegistryTitle" xml:space="preserve"><value>Create new registry</value></data>
|
||||
<data name="Home.Title" xml:space="preserve"><value>Title</value></data>
|
||||
<data name="Home.Type" xml:space="preserve"><value>Type</value></data>
|
||||
<data name="Home.Theme" xml:space="preserve"><value>Theme</value></data>
|
||||
<data name="Home.Theme.Default" xml:space="preserve"><value>Default</value></data>
|
||||
<data name="Home.Theme.Soft" xml:space="preserve"><value>Soft</value></data>
|
||||
<data name="Home.Theme.Modern" xml:space="preserve"><value>Modern</value></data>
|
||||
<data name="Home.RegistryType.Birth" xml:space="preserve"><value>Birth</value></data>
|
||||
<data name="Home.RegistryType.Wedding" xml:space="preserve"><value>Wedding</value></data>
|
||||
<data name="Home.RegistryType.Birthday" xml:space="preserve"><value>Birthday</value></data>
|
||||
<data name="Home.MustBeLoggedIn" xml:space="preserve"><value>You must be logged in to create a registry.</value></data>
|
||||
<data name="Home.TitleRequired" xml:space="preserve"><value>Title is required.</value></data>
|
||||
<data name="Home.LoginPromptPrefix" xml:space="preserve"><value>Please </value></data>
|
||||
<data name="Home.LoginPromptLinkText" xml:space="preserve"><value>log in</value></data>
|
||||
<data name="Home.LoginPromptSuffix" xml:space="preserve"><value> to create and manage registries.</value></data>
|
||||
|
||||
<data name="RegistryPublic.PageTitle" xml:space="preserve"><value>Registry</value></data>
|
||||
<data name="RegistryPublic.NotFound" xml:space="preserve"><value>Registry not found.</value></data>
|
||||
<data name="RegistryPublic.GoToAdmin" xml:space="preserve"><value>Go to Admin</value></data>
|
||||
<data name="RegistryPublic.ShippingAddress" xml:space="preserve"><value>Shipping address</value></data>
|
||||
<data name="RegistryPublic.BankTransferParticipation" xml:space="preserve"><value>Bank transfer participation</value></data>
|
||||
<data name="RegistryPublic.IBAN" xml:space="preserve"><value>IBAN</value></data>
|
||||
<data name="RegistryPublic.BIC" xml:space="preserve"><value>BIC</value></data>
|
||||
<data name="RegistryPublic.SecondHandPreferred" xml:space="preserve"><value>Second-hand preferred</value></data>
|
||||
<data name="RegistryPublic.SecondHandOptional" xml:space="preserve"><value>Second-hand optional</value></data>
|
||||
<data name="RegistryPublic.Qty" xml:space="preserve"><value>Qty:</value></data>
|
||||
<data name="RegistryPublic.PurchasedSuffix" xml:space="preserve"><value>purchased</value></data>
|
||||
<data name="RegistryPublic.Price" xml:space="preserve"><value>Price:</value></data>
|
||||
<data name="RegistryPublic.Participation" xml:space="preserve"><value>Participation:</value></data>
|
||||
<data name="RegistryPublic.OutOfFulfilled" xml:space="preserve"><value>out of {0}{1} fulfilled</value></data>
|
||||
<data name="RegistryPublic.FulfilledOnly" xml:space="preserve"><value>fulfilled</value></data>
|
||||
<data name="RegistryPublic.PurchasedBy" xml:space="preserve"><value>Purchased by:</value></data>
|
||||
<data name="RegistryPublic.ContributedBy" xml:space="preserve"><value>Contributed by:</value></data>
|
||||
<data name="RegistryPublic.YouQuantity" xml:space="preserve"><value>You ({0})</value></data>
|
||||
<data name="RegistryPublic.AndOtherPeople" xml:space="preserve"><value>and {0} other {1}</value></data>
|
||||
<data name="RegistryPublic.Person" xml:space="preserve"><value>person</value></data>
|
||||
<data name="RegistryPublic.People" xml:space="preserve"><value>people</value></data>
|
||||
<data name="RegistryPublic.AndOthers" xml:space="preserve"><value>and others</value></data>
|
||||
<data name="RegistryPublic.Purchased" xml:space="preserve"><value>Purchased</value></data>
|
||||
<data name="RegistryPublic.Contributed" xml:space="preserve"><value>Contributed</value></data>
|
||||
<data name="RegistryPublic.LoginToPurchase" xml:space="preserve"><value>Login to purchase</value></data>
|
||||
<data name="RegistryPublic.Purchase" xml:space="preserve"><value>Purchase</value></data>
|
||||
<data name="RegistryPublic.ManagePurchases" xml:space="preserve"><value>Manage purchases</value></data>
|
||||
<data name="RegistryPublic.EditPurchase" xml:space="preserve"><value>Edit purchase</value></data>
|
||||
<data name="RegistryPublic.MarkPurchased" xml:space="preserve"><value>Mark purchased</value></data>
|
||||
<data name="RegistryPublic.ManageParticipations" xml:space="preserve"><value>Manage participations</value></data>
|
||||
<data name="RegistryPublic.EditParticipation" xml:space="preserve"><value>Edit participation</value></data>
|
||||
<data name="RegistryPublic.PartiallyFulfill" xml:space="preserve"><value>Partially fulfill</value></data>
|
||||
<data name="RegistryPublic.MarkAsPurchased" xml:space="preserve"><value>Mark as purchased</value></data>
|
||||
<data name="RegistryPublic.HowManyUnitsPurchased" xml:space="preserve"><value>How many units did you purchase?</value></data>
|
||||
<data name="RegistryPublic.OpenProductLink" xml:space="preserve"><value>Open product link</value></data>
|
||||
<data name="RegistryPublic.ConfirmPurchase" xml:space="preserve"><value>Confirm purchase</value></data>
|
||||
<data name="RegistryPublic.LogContribution" xml:space="preserve"><value>Log contribution</value></data>
|
||||
<data name="RegistryPublic.TransferredAmount" xml:space="preserve"><value>Transferred amount</value></data>
|
||||
<data name="RegistryPublic.Confirm" xml:space="preserve"><value>Confirm</value></data>
|
||||
<data name="RegistryPublic.SelectPurchaserToUnmark" xml:space="preserve"><value>Select purchaser to unmark</value></data>
|
||||
<data name="RegistryPublic.MultipleUsersPurchased" xml:space="preserve"><value>Multiple users have purchased this item. Choose which purchase to unmark:</value></data>
|
||||
<data name="RegistryPublic.Unmark" xml:space="preserve"><value>Unmark</value></data>
|
||||
<data name="RegistryPublic.UnmarkAll" xml:space="preserve"><value>Unmark all</value></data>
|
||||
<data name="RegistryPublic.PartiallyFulfillItem" xml:space="preserve"><value>Partially fulfill item</value></data>
|
||||
<data name="RegistryPublic.SelectDonateMethod" xml:space="preserve"><value>Select how you want to donate:</value></data>
|
||||
<data name="RegistryPublic.IbanTransfer" xml:space="preserve"><value>IBAN transfer</value></data>
|
||||
<data name="RegistryPublic.SingleQrCode" xml:space="preserve"><value>Single QR code</value></data>
|
||||
<data name="RegistryPublic.QrCodePerAmount" xml:space="preserve"><value>QR code per amount</value></data>
|
||||
<data name="RegistryPublic.OpenPaymentLink" xml:space="preserve"><value>Open payment link</value></data>
|
||||
<data name="RegistryPublic.HowMuchAdded" xml:space="preserve"><value>How much did you add for this item?</value></data>
|
||||
<data name="RegistryPublic.NoQrForAmount" xml:space="preserve"><value>No QR code configured for this exact amount.</value></data>
|
||||
<data name="RegistryPublic.ManagePurchase" xml:space="preserve"><value>Manage purchase</value></data>
|
||||
<data name="RegistryPublic.ManageParticipation" xml:space="preserve"><value>Manage participation</value></data>
|
||||
<data name="RegistryPublic.User" xml:space="preserve"><value>User</value></data>
|
||||
|
||||
<data name="RegistryContributionAmount.PageTitle" xml:space="preserve"><value>Contribution Amount</value></data>
|
||||
<data name="RegistryContributionAmount.ItemNotFound" xml:space="preserve"><value>Item not found.</value></data>
|
||||
<data name="RegistryContributionAmount.LoginFirst" xml:space="preserve"><value>Please log in first.</value></data>
|
||||
<data name="RegistryContributionAmount.PartiallyFulfill" xml:space="preserve"><value>Partially fulfill: {0}</value></data>
|
||||
<data name="RegistryContributionAmount.NoRepresentableAmount" xml:space="preserve"><value>No representable amount can be formed from configured QR codes up to {0}200.</value></data>
|
||||
<data name="RegistryContributionAmount.SelectAmount" xml:space="preserve"><value>Select amount: {0}{1}</value></data>
|
||||
<data name="RegistryContributionAmount.NoQrCombination" xml:space="preserve"><value>No QR combination available for this amount.</value></data>
|
||||
<data name="RegistryContributionAmount.SuggestedCombination" xml:space="preserve"><value>Suggested QR combination:</value></data>
|
||||
<data name="RegistryContributionAmount.OpenPaymentLink" xml:space="preserve"><value>Open payment link</value></data>
|
||||
<data name="RegistryContributionAmount.TransferredAmount" xml:space="preserve"><value>I transferred this amount</value></data>
|
||||
|
||||
<data name="RegistryInvite.PageTitle" xml:space="preserve"><value>Admin Invite</value></data>
|
||||
<data name="RegistryInvite.Title" xml:space="preserve"><value>Admin invitation</value></data>
|
||||
<data name="RegistryInvite.Validating" xml:space="preserve"><value>Validating invitation...</value></data>
|
||||
<data name="RegistryInvite.Accepted" xml:space="preserve"><value>Invitation accepted. You are now an admin.</value></data>
|
||||
<data name="RegistryInvite.GoToAdmin" xml:space="preserve"><value>Go to admin</value></data>
|
||||
<data name="RegistryInvite.Invalid" xml:space="preserve"><value>The invitation is invalid or already used.</value></data>
|
||||
|
||||
<data name="RegistryAdmin.PageTitle" xml:space="preserve"><value>Registry Admin</value></data>
|
||||
<data name="RegistryAdmin.Title" xml:space="preserve"><value>Registry Admin</value></data>
|
||||
<data name="RegistryAdmin.SmtpNotConfigured" xml:space="preserve"><value>SMTP is not configured. Email features (identity emails and admin invite emails) are disabled. Configure the Smtp section in appsettings or user secrets.</value></data>
|
||||
<data name="RegistryAdmin.Tab.Items" xml:space="preserve"><value>Items</value></data>
|
||||
<data name="RegistryAdmin.Tab.Settings" xml:space="preserve"><value>Settings</value></data>
|
||||
<data name="RegistryAdmin.Tab.Administrators" xml:space="preserve"><value>Administrators</value></data>
|
||||
<data name="RegistryAdmin.Tab.Addresses" xml:space="preserve"><value>Addresses</value></data>
|
||||
<data name="RegistryAdmin.Tab.ActionLog" xml:space="preserve"><value>Action Log</value></data>
|
||||
<data name="RegistryAdmin.ViewPublicList" xml:space="preserve"><value>View Public List</value></data>
|
||||
<data name="RegistryAdmin.AddOrEditItem" xml:space="preserve"><value>Add or edit item</value></data>
|
||||
<data name="RegistryAdmin.Name" xml:space="preserve"><value>Name</value></data>
|
||||
<data name="RegistryAdmin.ProductUrl" xml:space="preserve"><value>Product URL</value></data>
|
||||
<data name="RegistryAdmin.AutoFetch" xml:space="preserve"><value>Auto fetch</value></data>
|
||||
<data name="RegistryAdmin.PictureUrl" xml:space="preserve"><value>Picture URL</value></data>
|
||||
<data name="RegistryAdmin.Description" xml:space="preserve"><value>Description</value></data>
|
||||
<data name="RegistryAdmin.Price" xml:space="preserve"><value>Price</value></data>
|
||||
<data name="RegistryAdmin.CurrencySymbol" xml:space="preserve"><value>Currency symbol</value></data>
|
||||
<data name="RegistryAdmin.DesiredQty" xml:space="preserve"><value>Desired qty</value></data>
|
||||
<data name="RegistryAdmin.Participation" xml:space="preserve"><value>Participation</value></data>
|
||||
<data name="RegistryAdmin.ParticipationTarget" xml:space="preserve"><value>Participation target</value></data>
|
||||
<data name="RegistryAdmin.SecondHandPreference" xml:space="preserve"><value>Second hand preference</value></data>
|
||||
<data name="RegistryAdmin.SecondHandOptional" xml:space="preserve"><value>Second hand optional</value></data>
|
||||
<data name="RegistryAdmin.PreferSecondHand" xml:space="preserve"><value>Prefer second hand</value></data>
|
||||
<data name="RegistryAdmin.NewOnly" xml:space="preserve"><value>New only</value></data>
|
||||
<data name="RegistryAdmin.Given" xml:space="preserve"><value>Given</value></data>
|
||||
<data name="RegistryAdmin.Category" xml:space="preserve"><value>Category</value></data>
|
||||
<data name="RegistryAdmin.SaveItem" xml:space="preserve"><value>Save item</value></data>
|
||||
<data name="RegistryAdmin.CategoriesAndItems" xml:space="preserve"><value>Categories and items</value></data>
|
||||
<data name="RegistryAdmin.NewCategory" xml:space="preserve"><value>New category</value></data>
|
||||
<data name="RegistryAdmin.AddCategory" xml:space="preserve"><value>Add category</value></data>
|
||||
<data name="RegistryAdmin.DragHint" xml:space="preserve"><value>Drag categories or items to reorder. Drop items into another category to regroup them.</value></data>
|
||||
<data name="RegistryAdmin.Rename" xml:space="preserve"><value>Rename</value></data>
|
||||
<data name="RegistryAdmin.DropItemsHere" xml:space="preserve"><value>Drop items here.</value></data>
|
||||
<data name="RegistryAdmin.Header.Name" xml:space="preserve"><value>Name</value></data>
|
||||
<data name="RegistryAdmin.Header.DesiredQty" xml:space="preserve"><value>Desired Qty</value></data>
|
||||
<data name="RegistryAdmin.Header.Condition" xml:space="preserve"><value>Condition</value></data>
|
||||
<data name="RegistryAdmin.Header.Participation" xml:space="preserve"><value>Participation</value></data>
|
||||
<data name="RegistryAdmin.Header.PurchasedContributedBy" xml:space="preserve"><value>Purchased by / Contributed by</value></data>
|
||||
<data name="RegistryAdmin.Purchased" xml:space="preserve"><value>Purchased:</value></data>
|
||||
<data name="RegistryAdmin.Contributed" xml:space="preserve"><value>Contributed:</value></data>
|
||||
<data name="RegistryAdmin.RegistrySettings" xml:space="preserve"><value>Registry Settings</value></data>
|
||||
<data name="RegistryAdmin.BabyName" xml:space="preserve"><value>Baby name</value></data>
|
||||
<data name="RegistryAdmin.BirthDate" xml:space="preserve"><value>Birth date</value></data>
|
||||
<data name="RegistryAdmin.Theme" xml:space="preserve"><value>Theme</value></data>
|
||||
<data name="RegistryAdmin.Theme.Default" xml:space="preserve"><value>Default</value></data>
|
||||
<data name="RegistryAdmin.Theme.Soft" xml:space="preserve"><value>Soft</value></data>
|
||||
<data name="RegistryAdmin.Theme.Modern" xml:space="preserve"><value>Modern</value></data>
|
||||
<data name="RegistryAdmin.ShippingAddress" xml:space="preserve"><value>Shipping address</value></data>
|
||||
<data name="RegistryAdmin.LineBreaksPreserved" xml:space="preserve"><value>Line breaks will be preserved</value></data>
|
||||
<data name="RegistryAdmin.HeroImage" xml:space="preserve"><value>Hero image</value></data>
|
||||
<data name="RegistryAdmin.HideHeaderName" xml:space="preserve"><value>Hide name heading on public page</value></data>
|
||||
<data name="RegistryAdmin.UploadImage" xml:space="preserve"><value>Upload image</value></data>
|
||||
<data name="RegistryAdmin.RemoveHeroImage" xml:space="preserve"><value>Remove hero image</value></data>
|
||||
<data name="RegistryAdmin.CurrentHeroImage" xml:space="preserve"><value>Current hero image</value></data>
|
||||
<data name="RegistryAdmin.TopContent" xml:space="preserve"><value>Top content</value></data>
|
||||
<data name="RegistryAdmin.WelcomeText" xml:space="preserve"><value>Welcome text</value></data>
|
||||
<data name="RegistryAdmin.BankAccountSettings" xml:space="preserve"><value>Bank Account Settings</value></data>
|
||||
<data name="RegistryAdmin.BankAccountName" xml:space="preserve"><value>Bank account name</value></data>
|
||||
<data name="RegistryAdmin.DisplayBankAccountName" xml:space="preserve"><value>Display bank account name</value></data>
|
||||
<data name="RegistryAdmin.ContributionPaymentOptions" xml:space="preserve"><value>Contribution Payment Options</value></data>
|
||||
<data name="RegistryAdmin.SingleQrCodeUrl" xml:space="preserve"><value>Single QR code URL</value></data>
|
||||
<data name="RegistryAdmin.SingleQrHelp" xml:space="preserve"><value>Optional: one QR code that donors can scan for any amount.</value></data>
|
||||
<data name="RegistryAdmin.AmountSpecificQrCodes" xml:space="preserve"><value>Amount-specific QR codes</value></data>
|
||||
<data name="RegistryAdmin.AddQrAmount" xml:space="preserve"><value>Add QR amount</value></data>
|
||||
<data name="RegistryAdmin.NoAmountSpecificQrCodes" xml:space="preserve"><value>No amount-specific QR codes configured.</value></data>
|
||||
<data name="RegistryAdmin.QrCodeUrl" xml:space="preserve"><value>QR code URL</value></data>
|
||||
<data name="RegistryAdmin.SaveSettings" xml:space="preserve"><value>Save settings</value></data>
|
||||
<data name="RegistryAdmin.UserAddresses" xml:space="preserve"><value>User addresses</value></data>
|
||||
<data name="RegistryAdmin.NoUsersFound" xml:space="preserve"><value>No users found yet.</value></data>
|
||||
<data name="RegistryAdmin.Email" xml:space="preserve"><value>Email</value></data>
|
||||
<data name="RegistryAdmin.Address" xml:space="preserve"><value>Address</value></data>
|
||||
<data name="RegistryAdmin.CurrentAdministrators" xml:space="preserve"><value>Current administrators</value></data>
|
||||
<data name="RegistryAdmin.NoAdmins" xml:space="preserve"><value>No admins assigned yet.</value></data>
|
||||
<data name="RegistryAdmin.EmailOrName" xml:space="preserve"><value>Email / Name</value></data>
|
||||
<data name="RegistryAdmin.InviteAdministrator" xml:space="preserve"><value>Invite administrator</value></data>
|
||||
<data name="RegistryAdmin.OptionalEmail" xml:space="preserve"><value>optional email</value></data>
|
||||
<data name="RegistryAdmin.CreateInvite" xml:space="preserve"><value>Create invite</value></data>
|
||||
<data name="RegistryAdmin.InviteLink" xml:space="preserve"><value>Invite link:</value></data>
|
||||
|
||||
<data name="RegistryActionLog.PageTitle" xml:space="preserve"><value>Action Log - Registry Admin</value></data>
|
||||
<data name="RegistryActionLog.Title" xml:space="preserve"><value>Registry Action Log</value></data>
|
||||
<data name="RegistryActionLog.Description" xml:space="preserve"><value>This log shows all user actions on this registry: purchases, contributions, and other interactions.</value></data>
|
||||
<data name="RegistryActionLog.NoActions" xml:space="preserve"><value>No actions recorded yet.</value></data>
|
||||
<data name="RegistryActionLog.DateTime" xml:space="preserve"><value>Date/Time</value></data>
|
||||
<data name="RegistryActionLog.User" xml:space="preserve"><value>User</value></data>
|
||||
<data name="RegistryActionLog.Action" xml:space="preserve"><value>Action</value></data>
|
||||
<data name="RegistryActionLog.Item" xml:space="preserve"><value>Item</value></data>
|
||||
<data name="RegistryActionLog.Quantity" xml:space="preserve"><value>Quantity</value></data>
|
||||
<data name="RegistryActionLog.Amount" xml:space="preserve"><value>Amount</value></data>
|
||||
<data name="RegistryActionLog.Details" xml:space="preserve"><value>Details</value></data>
|
||||
<data name="RegistryActionLog.Badge.RegistryOpened" xml:space="preserve"><value>Registry opened</value></data>
|
||||
<data name="RegistryActionLog.Badge.ItemLinkOpened" xml:space="preserve"><value>Item link opened</value></data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseMarked" xml:space="preserve"><value>Purchase marked</value></data>
|
||||
<data name="RegistryActionLog.Badge.PurchaseUnmarked" xml:space="preserve"><value>Purchase unmarked</value></data>
|
||||
<data name="RegistryActionLog.Badge.PartialPurchase" xml:space="preserve"><value>Partial purchase</value></data>
|
||||
<data name="RegistryActionLog.Badge.ContributionLogged" xml:space="preserve"><value>Contribution logged</value></data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchSucceeded" xml:space="preserve"><value>Auto fetch succeeded</value></data>
|
||||
<data name="RegistryActionLog.Badge.MetadataFetchFailed" xml:space="preserve"><value>Auto fetch failed</value></data>
|
||||
|
||||
<data name="Error.PageTitle" xml:space="preserve"><value>Error</value></data>
|
||||
<data name="Error.Title" xml:space="preserve"><value>Error.</value></data>
|
||||
<data name="Error.Subtitle" xml:space="preserve"><value>An error occurred while processing your request.</value></data>
|
||||
<data name="Error.RequestId" xml:space="preserve"><value>Request ID:</value></data>
|
||||
<data name="Error.DevMode" xml:space="preserve"><value>Development Mode</value></data>
|
||||
<data name="Error.DevHint1" xml:space="preserve"><value>Swapping to Development environment will display more detailed information about the error that occurred.</value></data>
|
||||
<data name="Error.DevHint2" xml:space="preserve"><value>The Development environment shouldn't be enabled for deployed applications.</value></data>
|
||||
<data name="Error.DevHint3" xml:space="preserve"><value>It can result in displaying sensitive information from exceptions to end users.</value></data>
|
||||
<data name="Error.DevHint4" xml:space="preserve"><value>For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development and restarting the app.</value></data>
|
||||
|
||||
<data name="MainLayout.UnhandledError" xml:space="preserve"><value>An unhandled error has occurred.</value></data>
|
||||
<data name="MainLayout.Reload" xml:space="preserve"><value>Reload</value></data>
|
||||
|
||||
<data name="NavMenu.Brand" xml:space="preserve"><value>BirthList</value></data>
|
||||
<data name="NavMenu.NavigationMenu" xml:space="preserve"><value>Navigation menu</value></data>
|
||||
<data name="NavMenu.Home" xml:space="preserve"><value>Home</value></data>
|
||||
<data name="NavMenu.Logout" xml:space="preserve"><value>Logout</value></data>
|
||||
<data name="NavMenu.Register" xml:space="preserve"><value>Register</value></data>
|
||||
<data name="NavMenu.Login" xml:space="preserve"><value>Login</value></data>
|
||||
|
||||
</root>
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace BirthList.Web;
|
||||
|
||||
public sealed class SharedResources
|
||||
{
|
||||
}
|
||||
@@ -39,5 +39,18 @@
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"PublicUrl": ""
|
||||
"PublicUrl": "",
|
||||
"AmazonMetadata": {
|
||||
// Amazon Product Advertising API 5.0 credentials.
|
||||
// Get these from your Amazon Associates account at https://affiliate-program.amazon.com
|
||||
// When AccessKey, SecretKey and AssociateTag are all set, the PA API is used as primary metadata source.
|
||||
"AccessKey": "",
|
||||
"SecretKey": "",
|
||||
"AssociateTag": "",
|
||||
// PA API marketplace host, e.g. "webservices.amazon.com" (US/default) or "webservices.amazon.com.be" (BE)
|
||||
"PaApiHost": "webservices.amazon.com",
|
||||
// RapidAPI key for the "Real-Time Amazon Data" API (https://rapidapi.com/letscrape-6bRBa3QguO5/api/real-time-amazon-data)
|
||||
// Used as fallback when direct scraping returns no metadata.
|
||||
"RapidApiKey": ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user