diff --git a/backend/src/Web/Features/Contents/Data/Creator.cs b/backend/src/Web/Features/Contents/Data/Creator.cs
index fd29df8..4eaf646 100644
--- a/backend/src/Web/Features/Contents/Data/Creator.cs
+++ b/backend/src/Web/Features/Contents/Data/Creator.cs
@@ -18,7 +18,8 @@ public class Creator
public bool AcceptDonation { get; set; }
public bool Verified { get; set; }
- public Slugs Slugs { get; set; } = null!;
+ [MaxLength(255)] public string Name { get; set; }
+ [MaxLength(128)] public string Slug { get; set; }
[MaxLength(255)] public string? Title { get; set; }
public Socials Socials { get; set; } = new();
public Images Images { get; set; } = new();
diff --git a/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.Designer.cs b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.Designer.cs
new file mode 100644
index 0000000..a252da1
--- /dev/null
+++ b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.Designer.cs
@@ -0,0 +1,382 @@
+//
+using System;
+using Hutopy.Web.Features.Contents.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Hutopy.Web.Features.Contents.Data.Migrations
+{
+ [DbContext(typeof(ContentDbContext))]
+ [Migration("20250415071053_SplitSlugFromCreator")]
+ partial class SplitSlugFromCreator
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("Content")
+ .HasAnnotation("ProductVersion", "9.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("timestamp with time zone")
+ .HasDefaultValueSql("CURRENT_TIMESTAMP");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("HtmlFileUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("ThumbnailUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.PrimitiveCollection("Urls")
+ .HasColumnType("text[]");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy");
+
+ b.ToTable("Contents", "Content");
+ });
+
+ modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AcceptDonation")
+ .HasColumnType("boolean");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedBy")
+ .HasColumnType("uuid");
+
+ b.Property("IsDeleted")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("boolean")
+ .HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Title")
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Verified")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Creators", "Content");
+ });
+
+ modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Slugs", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("NormalizedName")
+ .IsRequired()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", true);
+
+ b.Property("ReservedUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UsedBy")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique();
+
+ b.ToTable("Slugs", "Content");
+ });
+
+ modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
+ {
+ b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator")
+ .WithMany()
+ .HasForeignKey("CreatedBy");
+
+ b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 =>
+ {
+ b1.Property("ContentId")
+ .HasColumnType("uuid");
+
+ b1.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id"));
+
+ b1.Property("Reaction")
+ .HasColumnType("integer");
+
+ b1.Property("UserId")
+ .HasColumnType("uuid");
+
+ b1.Property("UserName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b1.HasKey("ContentId", "Id");
+
+ b1.ToTable("Reactions", "Content");
+
+ b1.WithOwner()
+ .HasForeignKey("ContentId");
+ });
+
+ b.Navigation("Creator");
+
+ b.Navigation("Reactions");
+ });
+
+ modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
+ {
+ b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 =>
+ {
+ b1.Property("CreatorId")
+ .HasColumnType("uuid");
+
+ b1.Property("Banner")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("Logo")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.HasKey("CreatorId");
+
+ b1.ToTable("Images", "Content");
+
+ b1.WithOwner()
+ .HasForeignKey("CreatorId");
+ });
+
+ b.OwnsOne("Hutopy.Web.Features.Contents.Data.PresentationInfos", "PresentationInfos", b1 =>
+ {
+ b1.Property("CreatorId")
+ .HasColumnType("uuid");
+
+ b1.Property("Email")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b1.Property("Image1Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("Image2Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("Image3Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("Image4Url")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("ImagesSubtitle")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b1.Property("ImagesText")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b1.Property("MainImageText")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b1.Property("MainImageUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("MainVideoText")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b1.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b1.Property("Title")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b1.Property("VideoSubtitle")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b1.Property("VideoSubtitleMain")
+ .IsRequired()
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)");
+
+ b1.Property("VideoText")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b1.Property("VideoUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("VideoUrlMain")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.HasKey("CreatorId");
+
+ b1.ToTable("PresentationInfos", "Content");
+
+ b1.WithOwner()
+ .HasForeignKey("CreatorId");
+ });
+
+ b.OwnsOne("Hutopy.Web.Features.Contents.Data.Socials", "Socials", b1 =>
+ {
+ b1.Property("CreatorId")
+ .HasColumnType("uuid");
+
+ b1.Property("FacebookUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("InstagramUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("LinkedInUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("RedditUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("TikTokUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("WebsiteUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("XUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.Property("YoutubeUrl")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b1.HasKey("CreatorId");
+
+ b1.ToTable("Socials", "Content");
+
+ b1.WithOwner()
+ .HasForeignKey("CreatorId");
+ });
+
+ b.Navigation("Images")
+ .IsRequired();
+
+ b.Navigation("PresentationInfos")
+ .IsRequired();
+
+ b.Navigation("Socials")
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.cs b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.cs
new file mode 100644
index 0000000..4ecee0a
--- /dev/null
+++ b/backend/src/Web/Features/Contents/Data/Migrations/20250415071053_SplitSlugFromCreator.cs
@@ -0,0 +1,111 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Hutopy.Web.Features.Contents.Data.Migrations
+{
+ ///
+ public partial class SplitSlugFromCreator : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Creators_Slugs_SlugsId",
+ schema: "Content",
+ table: "Creators");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Creators_SlugsId",
+ schema: "Content",
+ table: "Creators");
+
+ migrationBuilder.DropColumn(
+ name: "Active",
+ schema: "Content",
+ table: "Slugs");
+
+ migrationBuilder.DropColumn(
+ name: "SlugsId",
+ schema: "Content",
+ table: "Creators");
+
+ migrationBuilder.AddColumn(
+ name: "UsedBy",
+ schema: "Content",
+ table: "Slugs",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Name",
+ schema: "Content",
+ table: "Creators",
+ type: "character varying(255)",
+ maxLength: 255,
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn(
+ name: "Slug",
+ schema: "Content",
+ table: "Creators",
+ type: "character varying(128)",
+ maxLength: 128,
+ nullable: false,
+ defaultValue: "");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "UsedBy",
+ schema: "Content",
+ table: "Slugs");
+
+ migrationBuilder.DropColumn(
+ name: "Name",
+ schema: "Content",
+ table: "Creators");
+
+ migrationBuilder.DropColumn(
+ name: "Slug",
+ schema: "Content",
+ table: "Creators");
+
+ migrationBuilder.AddColumn(
+ name: "Active",
+ schema: "Content",
+ table: "Slugs",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "SlugsId",
+ schema: "Content",
+ table: "Creators",
+ type: "uuid",
+ nullable: false,
+ defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Creators_SlugsId",
+ schema: "Content",
+ table: "Creators",
+ column: "SlugsId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Creators_Slugs_SlugsId",
+ schema: "Content",
+ table: "Creators",
+ column: "SlugsId",
+ principalSchema: "Content",
+ principalTable: "Slugs",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ }
+ }
+}
diff --git a/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs b/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs
index c6adf85..410d562 100644
--- a/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs
+++ b/backend/src/Web/Features/Contents/Data/Migrations/ContentDbContextModelSnapshot.cs
@@ -97,8 +97,15 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
.HasColumnType("boolean")
.HasComputedColumnSql("\"DeletedAt\" IS NOT NULL", true);
- b.Property("SlugsId")
- .HasColumnType("uuid");
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("character varying(255)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
b.Property("Title")
.HasMaxLength(255)
@@ -109,8 +116,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
b.HasKey("Id");
- b.HasIndex("SlugsId");
-
b.ToTable("Creators", "Content");
});
@@ -120,9 +125,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
- b.Property("Active")
- .HasColumnType("boolean");
-
b.Property("CreatedAt")
.HasColumnType("timestamp with time zone");
@@ -144,6 +146,9 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
b.Property("ReservedUntil")
.HasColumnType("timestamp with time zone");
+ b.Property("UsedBy")
+ .HasColumnType("uuid");
+
b.HasKey("Id");
b.HasIndex("NormalizedName")
@@ -195,12 +200,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
{
- b.HasOne("Hutopy.Web.Features.Contents.Data.Slugs", "Slugs")
- .WithMany()
- .HasForeignKey("SlugsId")
- .OnDelete(DeleteBehavior.Cascade)
- .IsRequired();
-
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 =>
{
b1.Property("CreatorId")
@@ -371,8 +370,6 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
b.Navigation("PresentationInfos")
.IsRequired();
- b.Navigation("Slugs");
-
b.Navigation("Socials")
.IsRequired();
});
diff --git a/backend/src/Web/Features/Contents/Data/Slugs.cs b/backend/src/Web/Features/Contents/Data/Slugs.cs
index 8925dcf..803de2d 100644
--- a/backend/src/Web/Features/Contents/Data/Slugs.cs
+++ b/backend/src/Web/Features/Contents/Data/Slugs.cs
@@ -7,8 +7,8 @@ public class Slugs
public Guid Id { get; set; }
public Guid CreatedBy { get; set; }
public DateTimeOffset CreatedAt { get; init; }
+ public Guid? UsedBy { get; set; }
[MaxLength(128)] public string Name { get; set; } = null!;
[MaxLength(128)] public string NormalizedName { get; set; } = null!;
public DateTimeOffset ReservedUntil { get; set; }
- public bool Active { get; set; }
}
diff --git a/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs b/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs
index 5b93bd3..7f97f5e 100644
--- a/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs
+++ b/backend/src/Web/Features/Contents/Handlers/CreateContentFromHtml.cs
@@ -67,7 +67,7 @@ public sealed class PostContentHtml(
{
Id = c.Id,
CreatedBy = c.CreatedBy,
- CreatedByName = c.Creator!.Slugs.Name,
+ CreatedByName = c.Creator.Name,
CreatedByPortraitUrl = c.Creator.Images.Logo,
CreatedAt = c.CreatedAt,
DeletedBy = c.DeletedBy,
diff --git a/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs b/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs
index 83ae15c..285ee1c 100644
--- a/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs
+++ b/backend/src/Web/Features/Contents/Handlers/CreateCreator.cs
@@ -16,7 +16,7 @@ public sealed class CreateCreatorRequestValidator : Validator r.SlugReservationId)
.NotNull()
.NotEmpty()
- .WithMessage("You should specify a valid Name");
+ .WithMessage("You should specify a valid SlugReservationId");
RuleFor(r => r.CreatorId)
.NotNull()
@@ -48,7 +48,7 @@ public sealed class CreateCreatorHandler(
.Slugs
.SingleAsync(s => s.Id == req.SlugReservationId, ct);
- if (slug.Active
+ if (slug.UsedBy is not null
|| slug.ReservedUntil < DateTimeOffset.UtcNow
|| slug.CreatedBy != User.GetUserId())
{
@@ -56,14 +56,15 @@ public sealed class CreateCreatorHandler(
return;
}
- slug.Active = true;
+ slug.UsedBy = req.CreatorId;
await context.Creators.AddAsync(
new Creator
{
Id = req.CreatorId,
CreatedBy = User.GetUserId(),
- Slugs = slug
+ Name = slug.Name,
+ Slug = slug.NormalizedName,
},
ct);
diff --git a/backend/src/Web/Features/Contents/Handlers/GetContent.cs b/backend/src/Web/Features/Contents/Handlers/GetContent.cs
index beaa814..705d916 100644
--- a/backend/src/Web/Features/Contents/Handlers/GetContent.cs
+++ b/backend/src/Web/Features/Contents/Handlers/GetContent.cs
@@ -31,7 +31,7 @@ public class GetContent(
{
Id = c.Id,
CreatedBy = c.CreatedBy,
- CreatedByName = c.Creator!.Slugs.Name,
+ CreatedByName = c.Creator.Name,
CreatedByPortraitUrl = c.Creator.Images.Logo,
CreatedAt = c.CreatedAt,
DeletedBy = c.DeletedBy,
diff --git a/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs b/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs
index 78e09fb..ab6051a 100644
--- a/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs
+++ b/backend/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs
@@ -42,7 +42,7 @@ public class GetContentsByCreatorHandler(
{
Id = c.Id,
CreatedBy = c.CreatedBy,
- CreatedByName = c.Creator!.Slugs.Name,
+ CreatedByName = c.Creator.Name,
CreatedByPortraitUrl = c.Creator.Images.Logo,
CreatedAt = c.CreatedAt,
DeletedBy = c.DeletedBy,
diff --git a/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs b/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs
index f1d17c0..ba4ba0f 100644
--- a/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs
+++ b/backend/src/Web/Features/Contents/Handlers/GetCreatorBySlug.cs
@@ -65,7 +65,7 @@ public class GetCreatorBySlugHandler(
var creator = await context
.Creators
- .Where(c => EF.Functions.ILike(c.Slugs.Name, creatorName))
+ .Where(c => EF.Functions.ILike(c.Slug, creatorName))
.AsNoTracking()
.Select(c => new GetCreatorBySlugResponse
(
@@ -74,7 +74,7 @@ public class GetCreatorBySlugHandler(
c.CreatedAt,
c.Verified,
c.AcceptDonation,
- c.Slugs.NormalizedName,
+ c.Name,
c.Title,
c.Socials,
c.PresentationInfos,
diff --git a/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs b/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs
index dbb2205..1add899 100644
--- a/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs
+++ b/backend/src/Web/Features/Contents/Handlers/GetCreatorProfile.cs
@@ -42,8 +42,8 @@ public class GetCreatorProfileHandler(
Id = c.Id,
CreatedBy = c.CreatedBy,
CreatedAt = c.CreatedAt,
+ Name = c.Name,
Title = c.Title,
- Name = c.Slugs.NormalizedName,
Verified = c.Verified,
AcceptDonation = c.AcceptDonation,
Images = c.Images,
diff --git a/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs b/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs
index f213b9c..30cfc70 100644
--- a/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs
+++ b/backend/src/Web/Features/Contents/Handlers/GetFeaturedContents.cs
@@ -41,7 +41,7 @@ public class GetFeaturedContentsHandler(
{
Id = c.Id,
CreatedBy = c.CreatedBy,
- CreatedByName = c.Creator!.Slugs.Name,
+ CreatedByName = c.Creator.Name,
CreatedByPortraitUrl = c.Creator.Images.Logo,
CreatedAt = c.CreatedAt,
DeletedBy = c.DeletedBy,
diff --git a/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs b/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs
index b3f3030..fc31ea2 100644
--- a/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs
+++ b/backend/src/Web/Features/Contents/Handlers/RemoveCreator.cs
@@ -40,7 +40,6 @@ public sealed class RemoveCreatorHandler(
{
var creator = await context
.Creators
- .Include(c => c.Slugs)
.Where(c => c.Id == req.CreatorId)
.SingleOrDefaultAsync(cancellationToken: ct);
@@ -52,8 +51,6 @@ public sealed class RemoveCreatorHandler(
creator.DeletedAt = DateTimeOffset.UtcNow;
creator.DeletedBy = User.GetUserId();
-
- creator.Slugs.Active = false;
await context.SaveChangesAsync(ct);
diff --git a/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs b/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs
index 7bad1d4..7d83ee9 100644
--- a/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs
+++ b/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs
@@ -10,8 +10,8 @@ namespace Hutopy.Web.Features.Contents.Handlers;
[PublicAPI]
public record ReserveSlugRequest
{
- public string Slug { get; set; } = null!;
public required Guid ReservationId { get; set; }
+ public string Slug { get; set; } = null!;
}
[PublicAPI]
@@ -46,17 +46,26 @@ public sealed class ReserveSlug(
try
{
- await context.Slugs.AddAsync(
- new Slugs
- {
- Id = req.ReservationId,
- Active = false,
- Name = req.Slug,
- ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration,
- CreatedBy = User.GetUserId(),
- },
+ var reservation = await context.Slugs.FirstOrDefaultAsync(
+ s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
cancellationToken: ct);
+ if (reservation == null)
+ {
+ reservation = new Slugs
+ {
+ Id = req.ReservationId,
+ CreatedBy = User.GetUserId(),
+ CreatedAt = DateTimeOffset.UtcNow,
+ };
+
+ context.Slugs.Attach(reservation);
+ context.Entry(reservation).State = EntityState.Added;
+ }
+
+ reservation.Name = req.Slug;
+ reservation.ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration;
+
await context.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
diff --git a/frontend/src/views/creators/CreateCreator.vue b/frontend/src/views/creators/CreateCreator.vue
index e1484c4..85f6c09 100644
--- a/frontend/src/views/creators/CreateCreator.vue
+++ b/frontend/src/views/creators/CreateCreator.vue
@@ -19,7 +19,7 @@ const creatorProfileStore = useCreatorProfileStore();
const userProfileStore = useUserProfileStore();
function handleCreatorNameReservationIdChanged($event) {
- creatorNameReservationId.value = $event.value
+ creatorNameReservationId.value = $event
}
function cancel () {
@@ -70,7 +70,7 @@ async function createAccount() {
diff --git a/frontend/src/views/creators/NameEditor.vue b/frontend/src/views/creators/NameEditor.vue
index b1db705..366d55c 100644
--- a/frontend/src/views/creators/NameEditor.vue
+++ b/frontend/src/views/creators/NameEditor.vue
@@ -22,7 +22,7 @@ const isReserved = computed(() => reservationState.value === 'reserved');
const isOperationPending = ref(false);
const reservationState = ref(null);
-const reservationId = ref(null);
+const reservationId = ref(v7());
let timeout = null;
const handleInput = () => {
@@ -40,21 +40,19 @@ const checkNameAvailability = async () => {
}
try {
- const id = v7();
isOperationPending.value = true;
reservationState.value = "loading";
await client.post(
`/api/creators/@${encodeURIComponent(name.value)}/reserve`,
- {reservationId: id}
+ {reservationId: reservationId.value}
);
reservationState.value = "reserved";
- reservationId.value = id;
} catch (error) {
reservationState.value = "unavailable"; // Handle API failure case
reservationId.value = undefined;
} finally {
- emits('update:name', name);
- emits('update:creatorNameReservationId', reservationId);
+ emits('update:name', name.value);
+ emits('update:creatorNameReservationId', reservationId.value);
isOperationPending.value = false;
}
};