Transit
This commit is contained in:
@@ -8,6 +8,7 @@ public class ContentDbContext(
|
|||||||
|
|
||||||
public DbSet<Content> Contents => Set<Content>();
|
public DbSet<Content> Contents => Set<Content>();
|
||||||
public DbSet<Creator> Creators => Set<Creator>();
|
public DbSet<Creator> Creators => Set<Creator>();
|
||||||
|
public DbSet<Slugs> Slugs => Set<Slugs>();
|
||||||
|
|
||||||
protected override void OnModelCreating(
|
protected override void OnModelCreating(
|
||||||
ModelBuilder modelBuilder)
|
ModelBuilder modelBuilder)
|
||||||
@@ -36,12 +37,12 @@ public class ContentDbContext(
|
|||||||
.Property(c => c.ThumbnailUrl);
|
.Property(c => c.ThumbnailUrl);
|
||||||
|
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.Entity<Creator>()
|
.Entity<Slugs>()
|
||||||
.Property(x => x.NormalizedName)
|
.Property(x => x.NormalizedName)
|
||||||
.HasComputedColumnSql("LOWER( \"Content\".\"Creators\".\"Name\")", stored: true);
|
.HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", stored: true);
|
||||||
|
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.Entity<Creator>()
|
.Entity<Slugs>()
|
||||||
.HasIndex(x => x.NormalizedName)
|
.HasIndex(x => x.NormalizedName)
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ public class Creator
|
|||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
public bool AcceptDonation { get; set; }
|
public bool AcceptDonation { get; set; }
|
||||||
public bool Verified { get; set; }
|
public bool Verified { get; set; }
|
||||||
[MaxLength(255)] public string Name { get; set; } = null!;
|
public Slugs Slugs { get; set; } = null!;
|
||||||
[MaxLength(255)] public string NormalizedName { get; set; } = null!;
|
|
||||||
[MaxLength(255)] public string? Title { get; set; }
|
[MaxLength(255)] public string? Title { get; set; }
|
||||||
public Socials Socials { get; set; } = new();
|
public Socials Socials { get; set; } = new();
|
||||||
public Colors Colors { get; set; } = new();
|
public Colors Colors { get; set; } = new();
|
||||||
|
|||||||
442
backend/src/Web/Features/Contents/Data/Migrations/20250131210849_AddSlug.Designer.cs
generated
Normal file
442
backend/src/Web/Features/Contents/Data/Migrations/20250131210849_AddSlug.Designer.cs
generated
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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("20250131210849_AddSlug")]
|
||||||
|
partial class AddSlug
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("Content")
|
||||||
|
.HasAnnotation("ProductVersion", "8.0.10")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeletedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("HtmlFileUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailUrl")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string[]>("Urls")
|
||||||
|
.HasColumnType("text[]");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedBy");
|
||||||
|
|
||||||
|
b.ToTable("Contents", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("AcceptDonation")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("SlugsId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("Verified")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SlugsId");
|
||||||
|
|
||||||
|
b.ToTable("Creators", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Slugs", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", true);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReservedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
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")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("ContentId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||||
|
|
||||||
|
b1.Property<int>("Reaction")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("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.HasOne("Hutopy.Web.Features.Contents.Data.Slugs", "Slugs")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SlugsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Colors", "Colors", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Background")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("Error")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("OnBackground")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("OnError")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("OnPrimary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("OnSecondary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("OnSurface")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("Primary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("Secondary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.Property<string>("Surface")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(9)
|
||||||
|
.HasColumnType("character varying(9)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Colors", "Content");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Banner")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("Logo")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Images", "Content");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Hutopy.Web.Features.Contents.Data.PresentationInfos", "PresentationInfos", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("Image1Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("Image2Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("Image3Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("Image4Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("ImagesSubtitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("ImagesText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("character varying(10000)");
|
||||||
|
|
||||||
|
b1.Property<string>("MainImageText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("character varying(10000)");
|
||||||
|
|
||||||
|
b1.Property<string>("MainImageUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("MainVideoText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("character varying(10000)");
|
||||||
|
|
||||||
|
b1.Property<string>("PhoneNumber")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoSubtitle")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoSubtitleMain")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10000)
|
||||||
|
.HasColumnType("character varying(10000)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoUrl")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.Property<string>("VideoUrlMain")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("PresentationInfos", "Content");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Socials", "Socials", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("CreatorId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("FacebookUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("InstagramUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("LinkedInUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("RedditUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("TikTokUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("WebsiteUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("XUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.Property<string>("YoutubeUrl")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b1.HasKey("CreatorId");
|
||||||
|
|
||||||
|
b1.ToTable("Socials", "Content");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("CreatorId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Colors")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Images")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("PresentationInfos")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Slugs");
|
||||||
|
|
||||||
|
b.Navigation("Socials")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Contents.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSlug : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Creators_NormalizedName",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "NormalizedName",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators");
|
||||||
|
|
||||||
|
// Add SlugsId column to Creators (temporary nullable to avoid issues while updating)
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
// Create the Slugs table
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Slugs",
|
||||||
|
schema: "Content",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, computedColumnSql: "LOWER( \"Content\".\"Slugs\".\"Name\")", stored: true),
|
||||||
|
ReservedUntil = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Active = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Slugs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Slugs for existing Creators
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
INSERT INTO ""Content"".""Slugs"" (""Id"", ""Name"", ""CreatedBy"", ""CreatedAt"", ""ReservedUntil"", ""Active"")
|
||||||
|
SELECT ""Id"", ""Name"", ""CreatedBy"", ""CreatedAt"", ""CreatedAt"", TRUE
|
||||||
|
FROM ""Content"".""Creators""
|
||||||
|
WHERE ""Name"" IS NOT NULL
|
||||||
|
");
|
||||||
|
|
||||||
|
// Update Creators to reference Slugs
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
UPDATE ""Content"".""Creators""
|
||||||
|
SET ""SlugsId"" = (SELECT ""Id"" FROM ""Content"".""Slugs"" WHERE ""Content"".""Slugs"".""Name"" = ""Content"".""Creators"".""Name"")
|
||||||
|
WHERE ""Name"" IS NOT NULL
|
||||||
|
");
|
||||||
|
|
||||||
|
// Make SlugsId non-nullable
|
||||||
|
migrationBuilder.AlterColumn<Guid>(
|
||||||
|
name: "SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(Guid),
|
||||||
|
oldType: "uuid",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
// Create index for SlugsId
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Creators_SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
column: "SlugsId");
|
||||||
|
|
||||||
|
// Create index for NormalizedName in Slugs
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Slugs_NormalizedName",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Slugs",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
// Add foreign key constraint
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Creators_Slugs_SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
column: "SlugsId",
|
||||||
|
principalSchema: "Content",
|
||||||
|
principalTable: "Slugs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
// Drop the Name column
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Name",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Creators_Slugs_SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Slugs",
|
||||||
|
schema: "Content");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Creators_SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SlugsId",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
type: "character varying(255)",
|
||||||
|
maxLength: 255,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "NormalizedName",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
type: "character varying(255)",
|
||||||
|
maxLength: 255,
|
||||||
|
nullable: false,
|
||||||
|
computedColumnSql: "LOWER( \"Content\".\"Creators\".\"Name\")",
|
||||||
|
stored: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Creators_NormalizedName",
|
||||||
|
schema: "Content",
|
||||||
|
table: "Creators",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,17 +86,8 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
b.Property<Guid>("CreatedBy")
|
b.Property<Guid>("CreatedBy")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<Guid>("SlugsId")
|
||||||
.IsRequired()
|
.HasColumnType("uuid");
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("character varying(255)");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAddOrUpdate()
|
|
||||||
.HasMaxLength(255)
|
|
||||||
.HasColumnType("character varying(255)")
|
|
||||||
.HasComputedColumnSql("LOWER( \"Content\".\"Creators\".\"Name\")", true);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
@@ -107,10 +98,47 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SlugsId");
|
||||||
|
|
||||||
|
b.ToTable("Creators", "Content");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Slugs", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedBy")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComputedColumnSql("LOWER( \"Content\".\"Slugs\".\"Name\")", true);
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReservedUntil")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
b.HasIndex("NormalizedName")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Creators", "Content");
|
b.ToTable("Slugs", "Content");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
|
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b =>
|
||||||
@@ -158,6 +186,12 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>
|
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.Colors", "Colors", b1 =>
|
b.OwnsOne("Hutopy.Web.Features.Contents.Data.Colors", "Colors", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<Guid>("CreatorId")
|
b1.Property<Guid>("CreatorId")
|
||||||
@@ -394,6 +428,8 @@ namespace Hutopy.Web.Features.Contents.Data.Migrations
|
|||||||
b.Navigation("PresentationInfos")
|
b.Navigation("PresentationInfos")
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Slugs");
|
||||||
|
|
||||||
b.Navigation("Socials")
|
b.Navigation("Socials")
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|||||||
14
backend/src/Web/Features/Contents/Data/Slugs.cs
Normal file
14
backend/src/Web/Features/Contents/Data/Slugs.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Contents.Data;
|
||||||
|
|
||||||
|
public class Slugs
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid CreatedBy { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Web.Features.Contents.Data;
|
||||||
|
using Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents;
|
namespace Hutopy.Web.Features.Contents;
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
builder.Services.AddDbContext<ContentDbContext>(configureAction);
|
builder.Services.AddDbContext<ContentDbContext>(configureAction);
|
||||||
builder.Services.AddScoped<ContentDbContextInitializer>();
|
builder.Services.AddScoped<ContentDbContextInitializer>();
|
||||||
|
builder.Services.Configure<ContentOptions>(builder.Configuration.GetSection(ContentOptions.ConfigurationSection));
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
|
public class ContentOptions
|
||||||
|
{
|
||||||
|
public const string ConfigurationSection = "Contents";
|
||||||
|
|
||||||
|
public TimeSpan SlugReservationDuration { get; set; }
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ public sealed class PostContentHtml(
|
|||||||
{
|
{
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
CreatedBy = c.CreatedBy,
|
CreatedBy = c.CreatedBy,
|
||||||
CreatedByName = c.Creator!.Name,
|
CreatedByName = c.Creator!.Slugs.Name,
|
||||||
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
||||||
CreatedAt = c.CreatedAt,
|
CreatedAt = c.CreatedAt,
|
||||||
DeletedBy = c.DeletedBy,
|
DeletedBy = c.DeletedBy,
|
||||||
|
|||||||
@@ -1,28 +1,27 @@
|
|||||||
using System.Net;
|
using Hutopy.Web.Common.Security;
|
||||||
using FluentValidation.Results;
|
|
||||||
using Hutopy.Web.Common.Security;
|
|
||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Web.Features.Contents.Data;
|
||||||
using Npgsql;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record CreateCreatorRequest(
|
public record CreateCreatorRequest(
|
||||||
Guid CreatorId,
|
Guid SlugReservationId,
|
||||||
string Name);
|
Guid CreatorId);
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorRequest>
|
public sealed class CreateCreatorRequestValidator : Validator<CreateCreatorRequest>
|
||||||
{
|
{
|
||||||
public CreateCreatorRequestValidator()
|
public CreateCreatorRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(r => r.CreatorId)
|
RuleFor(r => r.SlugReservationId)
|
||||||
.NotNull().WithMessage("You should specify the CreatorId")
|
.NotNull()
|
||||||
.NotEmpty().WithMessage("You should specify a valid/not empty CreatorId");
|
.NotEmpty()
|
||||||
|
.WithMessage("You should specify a valid Name");
|
||||||
|
|
||||||
RuleFor(r => r.Name)
|
RuleFor(r => r.CreatorId)
|
||||||
.NotNull().WithMessage("You should specify the Name")
|
.NotNull()
|
||||||
.NotEmpty().WithMessage("You should specify a valid/not empty Name");
|
.NotEmpty()
|
||||||
|
.WithMessage("You should specify a valid CreatorId");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,14 +40,30 @@ public sealed class CreateCreatorHandler(
|
|||||||
CreateCreatorRequest req,
|
CreateCreatorRequest req,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
await using var transaction = await context.Database.BeginTransactionAsync(ct);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var slug = await context
|
||||||
|
.Slugs
|
||||||
|
.SingleAsync(s => s.Id == req.SlugReservationId, ct);
|
||||||
|
|
||||||
|
if (slug.Active == false
|
||||||
|
&& slug.ReservedUntil >= DateTime.Now
|
||||||
|
&& slug.CreatedBy == User.GetUserId())
|
||||||
|
{
|
||||||
|
await SendErrorsAsync(500, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slug.Active = true;
|
||||||
|
|
||||||
await context.Creators.AddAsync(
|
await context.Creators.AddAsync(
|
||||||
new Creator
|
new Creator
|
||||||
{
|
{
|
||||||
Id = req.CreatorId,
|
Id = req.CreatorId,
|
||||||
CreatedBy = User.GetUserId(),
|
CreatedBy = User.GetUserId(),
|
||||||
Name = req.Name,
|
Slugs = slug,
|
||||||
Colors =
|
Colors =
|
||||||
{
|
{
|
||||||
Primary = "#A30E79",
|
Primary = "#A30E79",
|
||||||
@@ -67,25 +82,13 @@ public sealed class CreateCreatorHandler(
|
|||||||
|
|
||||||
await context.SaveChangesAsync(ct);
|
await context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
await transaction.CommitAsync(ct);
|
||||||
|
|
||||||
await SendOkAsync(ct);
|
await SendOkAsync(ct);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
if (e.InnerException is PostgresException innerException)
|
await transaction.RollbackAsync(ct);
|
||||||
{
|
|
||||||
if (innerException.ConstraintName == "IX_Creators_NormalizedName")
|
|
||||||
{
|
|
||||||
await SendResultAsync(new ProblemDetails(
|
|
||||||
[new ValidationFailure(nameof(Creator.Name), "The name is already taken.")],
|
|
||||||
(int)HttpStatusCode.Conflict));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await SendResultAsync(new ProblemDetails(
|
|
||||||
[new ValidationFailure(nameof(Creator.Name), e.Message)],
|
|
||||||
(int)HttpStatusCode.Conflict));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public class GetContent(
|
|||||||
{
|
{
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
CreatedBy = c.CreatedBy,
|
CreatedBy = c.CreatedBy,
|
||||||
CreatedByName = c.Creator!.Name,
|
CreatedByName = c.Creator!.Slugs.Name,
|
||||||
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
||||||
CreatedAt = c.CreatedAt,
|
CreatedAt = c.CreatedAt,
|
||||||
DeletedBy = c.DeletedBy,
|
DeletedBy = c.DeletedBy,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class GetContentsByCreatorHandler(
|
|||||||
{
|
{
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
CreatedBy = c.CreatedBy,
|
CreatedBy = c.CreatedBy,
|
||||||
CreatedByName = c.Creator!.Name,
|
CreatedByName = c.Creator!.Slugs.Name,
|
||||||
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
||||||
CreatedAt = c.CreatedAt,
|
CreatedAt = c.CreatedAt,
|
||||||
DeletedBy = c.DeletedBy,
|
DeletedBy = c.DeletedBy,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Hutopy.Web.Features.Contents.Data;
|
using Hutopy.Web.Features.Contents.Data;
|
||||||
using Hutopy.Web.Features.Contents.Handlers.Models;
|
|
||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
@@ -10,18 +9,31 @@ public sealed class GetCreatorByAliasRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public record struct GetCreatorByAliasResponse(
|
public class GetCreatorByAliasResponse(
|
||||||
Guid Id,
|
Guid id,
|
||||||
Guid CreatedBy,
|
Guid createdBy,
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset createdAt,
|
||||||
bool Verified,
|
bool verified,
|
||||||
bool AcceptDonation,
|
bool acceptDonation,
|
||||||
string Name,
|
string name,
|
||||||
string? Title,
|
string? title,
|
||||||
Socials Socials,
|
Socials socials,
|
||||||
Colors Colors,
|
Colors colors,
|
||||||
PresentationInfos PresentationInfos,
|
PresentationInfos presentationInfos,
|
||||||
Images Images);
|
Images images)
|
||||||
|
{
|
||||||
|
public Guid Id { get; } = id;
|
||||||
|
public Guid CreatedBy { get; } = createdBy;
|
||||||
|
public DateTimeOffset CreatedAt { get; } = createdAt;
|
||||||
|
public bool Verified { get; } = verified;
|
||||||
|
public bool AcceptDonation { get; } = acceptDonation;
|
||||||
|
public string Name { get; } = name;
|
||||||
|
public string? Title { get; } = title;
|
||||||
|
public Socials Socials { get; } = socials;
|
||||||
|
public Colors Colors { get; } = colors;
|
||||||
|
public PresentationInfos PresentationInfos { get; } = presentationInfos;
|
||||||
|
public Images Images { get; } = images;
|
||||||
|
}
|
||||||
|
|
||||||
[UsedImplicitly]
|
[UsedImplicitly]
|
||||||
public sealed class GetCreatorByAliasRequestValidator
|
public sealed class GetCreatorByAliasRequestValidator
|
||||||
@@ -55,8 +67,22 @@ public class GetCreatorByAliasHandler(
|
|||||||
|
|
||||||
var creator = await context
|
var creator = await context
|
||||||
.Creators
|
.Creators
|
||||||
.Where(c => EF.Functions.ILike(c.Name, creatorName))
|
.Where(c => EF.Functions.ILike(c.Slugs.Name, creatorName))
|
||||||
.FirstOrDefaultAsync(ct);
|
.AsNoTracking()
|
||||||
|
.Select(c => new GetCreatorByAliasResponse
|
||||||
|
(
|
||||||
|
c.Id,
|
||||||
|
c.CreatedBy,
|
||||||
|
c.CreatedAt,
|
||||||
|
c.Verified,
|
||||||
|
c.AcceptDonation,
|
||||||
|
c.Slugs.NormalizedName,
|
||||||
|
c.Title,
|
||||||
|
c.Socials,
|
||||||
|
c.Colors,
|
||||||
|
c.PresentationInfos,
|
||||||
|
c.Images))
|
||||||
|
.SingleOrDefaultAsync(ct);
|
||||||
|
|
||||||
if (creator is null)
|
if (creator is null)
|
||||||
{
|
{
|
||||||
@@ -64,20 +90,7 @@ public class GetCreatorByAliasHandler(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var model = new GetCreatorByAliasResponse(
|
await SendAsync(creator, cancellation: ct);
|
||||||
creator.Id,
|
|
||||||
creator.CreatedBy,
|
|
||||||
creator.CreatedAt,
|
|
||||||
creator.Verified,
|
|
||||||
creator.AcceptDonation,
|
|
||||||
creator.Name,
|
|
||||||
creator.Title,
|
|
||||||
creator.Socials,
|
|
||||||
creator.Colors,
|
|
||||||
creator.PresentationInfos,
|
|
||||||
creator.Images);
|
|
||||||
|
|
||||||
await SendAsync(model, cancellation: ct);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,26 @@ using Hutopy.Web.Features.Contents.Data;
|
|||||||
|
|
||||||
namespace Hutopy.Web.Features.Contents.Handlers;
|
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public sealed class GetCreatorProfileResponse
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid CreatedBy { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool Verified { get; set; }
|
||||||
|
public bool AcceptDonation { get; set; }
|
||||||
|
public Colors Colors { get; set; }
|
||||||
|
public Images Images { get; set; }
|
||||||
|
public PresentationInfos PresentationInfos { get; set; }
|
||||||
|
public Socials Socials { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public class GetCreatorProfileHandler(
|
public class GetCreatorProfileHandler(
|
||||||
ContentDbContext context)
|
ContentDbContext context)
|
||||||
: EndpointWithoutRequest<Creator>
|
: EndpointWithoutRequest<GetCreatorProfileResponse>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
{
|
{
|
||||||
@@ -21,9 +37,23 @@ public class GetCreatorProfileHandler(
|
|||||||
{
|
{
|
||||||
var creator = await context
|
var creator = await context
|
||||||
.Creators
|
.Creators
|
||||||
.FindAsync(
|
.Where(c => c.Id == HttpContext.User.GetUserId())
|
||||||
[HttpContext.User.GetUserId()],
|
.AsNoTracking()
|
||||||
cancellationToken: ct);
|
.Select(c => new GetCreatorProfileResponse
|
||||||
|
{
|
||||||
|
Id = c.Id,
|
||||||
|
CreatedBy = c.CreatedBy,
|
||||||
|
CreatedAt = c.CreatedAt,
|
||||||
|
Title = c.Title,
|
||||||
|
Name = c.Slugs.NormalizedName,
|
||||||
|
Verified = c.Verified,
|
||||||
|
AcceptDonation = c.AcceptDonation,
|
||||||
|
Colors = c.Colors,
|
||||||
|
Images = c.Images,
|
||||||
|
PresentationInfos = c.PresentationInfos,
|
||||||
|
Socials = c.Socials,
|
||||||
|
})
|
||||||
|
.SingleOrDefaultAsync(ct);
|
||||||
|
|
||||||
if (creator is null) await SendNotFoundAsync(ct);
|
if (creator is null) await SendNotFoundAsync(ct);
|
||||||
else await SendAsync(creator, cancellation: ct);
|
else await SendAsync(creator, cancellation: ct);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class GetFeaturedContentsHandler(
|
|||||||
{
|
{
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
CreatedBy = c.CreatedBy,
|
CreatedBy = c.CreatedBy,
|
||||||
CreatedByName = c.Creator!.Name,
|
CreatedByName = c.Creator!.Slugs.Name,
|
||||||
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
CreatedByPortraitUrl = c.Creator.Images.Logo,
|
||||||
CreatedAt = c.CreatedAt,
|
CreatedAt = c.CreatedAt,
|
||||||
DeletedBy = c.DeletedBy,
|
DeletedBy = c.DeletedBy,
|
||||||
|
|||||||
95
backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs
Normal file
95
backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using System.Net;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using Hutopy.Web.Common.Security;
|
||||||
|
using Hutopy.Web.Features.Contents.Data;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace Hutopy.Web.Features.Contents.Handlers;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public record ReserveSlugRequest
|
||||||
|
{
|
||||||
|
public string Slug { get; set; } = null!;
|
||||||
|
public required Guid ReservationId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public sealed class ReserveSlugRequestValidator : Validator<ReserveSlugRequest>
|
||||||
|
{
|
||||||
|
public ReserveSlugRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(r => r.Slug)
|
||||||
|
.NotEmpty()
|
||||||
|
.NotNull()
|
||||||
|
.WithMessage("You should specify a valid Slug");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public sealed class ReserveSlug(
|
||||||
|
ContentDbContext context,
|
||||||
|
IOptions<ContentOptions> opts)
|
||||||
|
: Endpoint<ReserveSlugRequest>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/creators/@{Slug}/reserve");
|
||||||
|
Options(o => o.WithTags("Contents"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(
|
||||||
|
ReserveSlugRequest req,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
await using var transaction = await context.Database.BeginTransactionAsync(ct);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await context.Slugs.AddAsync(
|
||||||
|
new Slugs
|
||||||
|
{
|
||||||
|
Id = req.ReservationId,
|
||||||
|
Active = false,
|
||||||
|
Name = req.Slug,
|
||||||
|
ReservedUntil = DateTimeOffset.UtcNow + opts.Value.SlugReservationDuration,
|
||||||
|
CreatedBy = User.GetUserId(),
|
||||||
|
},
|
||||||
|
cancellationToken: ct);
|
||||||
|
|
||||||
|
await context.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
await transaction.CommitAsync(ct);
|
||||||
|
|
||||||
|
await SendOkAsync(new { Message = "Slug reserved." }, ct);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(ct);
|
||||||
|
|
||||||
|
Logger.LogError("Transaction failed: {Message}", e.Message);
|
||||||
|
|
||||||
|
if (e.InnerException is PostgresException innerException)
|
||||||
|
{
|
||||||
|
if (innerException.ConstraintName == "IX_Slugs_NormalizedName")
|
||||||
|
{
|
||||||
|
await SendResultAsync(new ProblemDetails(
|
||||||
|
[
|
||||||
|
new ValidationFailure(nameof(Slugs.Name),
|
||||||
|
"The name is already taken.")
|
||||||
|
],
|
||||||
|
(int)HttpStatusCode.Conflict));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SendResultAsync(new ProblemDetails(
|
||||||
|
[
|
||||||
|
new ValidationFailure(nameof(Slugs.Name),
|
||||||
|
e.Message)
|
||||||
|
],
|
||||||
|
(int)HttpStatusCode.Conflict));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,5 +12,8 @@
|
|||||||
"Jwt": {
|
"Jwt": {
|
||||||
"Lifetime": "00:30:00"
|
"Lifetime": "00:30:00"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Contents": {
|
||||||
|
"SlugReservationDuration": "00:05:00"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ import LoginView from '../views/LoginView.vue';
|
|||||||
import PaymentCompleted from '../views/PaymentCompleted.vue';
|
import PaymentCompleted from '../views/PaymentCompleted.vue';
|
||||||
import Home from '../views/main/Home.vue';
|
import Home from '../views/main/Home.vue';
|
||||||
import Wallet from '../views/main/Wallet.vue';
|
import Wallet from '../views/main/Wallet.vue';
|
||||||
import CreateCreator from "@/views/profile/creators/CreateCreator.vue";
|
import CreateCreator from "@/views/creators/CreateCreator.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const useAuthStore = defineStore(
|
|||||||
const refreshToken = useSessionStorage('auth-refreshToken', undefined)
|
const refreshToken = useSessionStorage('auth-refreshToken', undefined)
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!accessToken.value)
|
const isAuthenticated = computed(() => !!accessToken.value)
|
||||||
|
|
||||||
const userId = computed(() => {
|
const userId = computed(() => {
|
||||||
const claims = getClaimsFromToken(accessToken.value)
|
const claims = getClaimsFromToken(accessToken.value)
|
||||||
return claims.sub;
|
return claims.sub;
|
||||||
@@ -99,4 +100,3 @@ export const useAuthStore = defineStore(
|
|||||||
|
|
||||||
return {accessToken, refreshToken, isAuthenticated, userId, login, loginWithGoogle, logout}
|
return {accessToken, refreshToken, isAuthenticated, userId, login, loginWithGoogle, logout}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ export const useBrandingStore = defineStore(
|
|||||||
if (newCreator !== undefined) {
|
if (newCreator !== undefined) {
|
||||||
value.value = await fetchCreatorData(newCreator)
|
value.value = await fetchCreatorData(newCreator)
|
||||||
currentBrand.value = newCreator
|
currentBrand.value = newCreator
|
||||||
colors.value = value.value.colors
|
colors.value = value.value?.colors
|
||||||
presentationInfos.value = value.value.presentationInfos
|
presentationInfos.value = value.value?.presentationInfos
|
||||||
} else {
|
} else {
|
||||||
value.value = {}
|
value.value = {}
|
||||||
currentBrand.value = undefined
|
currentBrand.value = undefined
|
||||||
|
|||||||
@@ -1,67 +1,70 @@
|
|||||||
import { useClient } from '@/plugins/api.js';
|
import {useClient} from '@/plugins/api.js';
|
||||||
import { useAuthStore } from '@/stores/authStore.js';
|
import {useAuthStore} from '@/stores/authStore.js';
|
||||||
import { useSessionStorage } from '@vueuse/core';
|
import {useSessionStorage} from '@vueuse/core';
|
||||||
import { defineStore } from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import { computed, watch } from 'vue';
|
import {computed, watch} from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
|
|
||||||
export const useCreatorProfileStore = defineStore('creator-profile', () => {
|
export const useCreatorProfileStore = defineStore(
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => authStore.isAuthenticated,
|
|
||||||
async (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
await fetchCurrentCreatorProfile();
|
|
||||||
|
|
||||||
if (value.value === undefined) {
|
|
||||||
await router.push('/');
|
|
||||||
} else {
|
|
||||||
await router.push(`/@${value.value.name}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const value = useSessionStorage(
|
|
||||||
'creator-profile',
|
'creator-profile',
|
||||||
undefined,
|
() => {
|
||||||
{ writeDefaults: false }
|
const router = useRouter();
|
||||||
);
|
const authStore = useAuthStore();
|
||||||
|
watch(
|
||||||
|
() => authStore.isAuthenticated,
|
||||||
|
async (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
await fetchCurrentCreatorProfile();
|
||||||
|
if (value.value === undefined) {
|
||||||
|
await router.push('/');
|
||||||
|
} else {
|
||||||
|
await router.push(`/@${value.value.name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const hasCreator = computed(
|
const value = useSessionStorage(
|
||||||
() => value.value && Object.getOwnPropertyNames(value.value).length >= 1
|
'creator-profile',
|
||||||
);
|
{},
|
||||||
|
{writeDefaults: false}
|
||||||
|
);
|
||||||
|
|
||||||
const client = useClient();
|
const hasCreator = computed(
|
||||||
|
() => value.value && Object.getOwnPropertyNames(value.value).length >= 1
|
||||||
|
);
|
||||||
|
|
||||||
async function fetchCurrentCreatorProfile() {
|
const client = useClient();
|
||||||
try {
|
|
||||||
const creatorResponse = await client.get(`/api/creators/profile`);
|
|
||||||
value.value = creatorResponse.data;
|
|
||||||
// TODO: no cache-busting ???
|
|
||||||
} catch (error) {
|
|
||||||
value.value = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ConfigureStripeAccount() {
|
async function fetchCurrentCreatorProfile() {
|
||||||
try {
|
try {
|
||||||
await client.post(`/api/membership/stripe-account`);
|
const creatorResponse = await client.get(`/api/creators/profile`);
|
||||||
return true;
|
console.log('creatorProfile');
|
||||||
} catch (error) {
|
console.dir(creatorResponse.data)
|
||||||
return false;
|
value.value = creatorResponse.data;
|
||||||
}
|
console.dir(value.value);
|
||||||
}
|
// TODO: no cache-busting ???
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`!!!`)
|
||||||
|
value.value = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
async function ConfigureStripeAccount() {
|
||||||
creator: value,
|
try {
|
||||||
hasCreator,
|
await client.post(`/api/membership/stripe-account`);
|
||||||
fetchCurrentCreatorProfile,
|
return true;
|
||||||
ConfigureStripeAccount,
|
} catch (error) {
|
||||||
};
|
return false;
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
creator: value,
|
||||||
|
hasCreator,
|
||||||
|
fetchCurrentCreatorProfile,
|
||||||
|
ConfigureStripeAccount,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
65
frontend/src/views/creators/ActualBanner.vue
Normal file
65
frontend/src/views/creators/ActualBanner.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Banner Container with mouse events -->
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
@mouseenter="showTint = isCurrentCreator"
|
||||||
|
@mouseleave="showTint = false"
|
||||||
|
@click="isCurrentCreator && openBannerEditor()"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="w-full drop-shadow-[0_10px_6px_rgba(0,0,0,0.25)] h-60"
|
||||||
|
:src="brandingStore.value.images.banner ? brandingStore.value.images.banner : '/images/placeholders/banner.png'"
|
||||||
|
alt="Profile Banner"
|
||||||
|
>
|
||||||
|
<!-- Tint Effect -->
|
||||||
|
<div
|
||||||
|
v-if="showTint"
|
||||||
|
class="absolute inset-0 bg-black/25 cursor-pointer"
|
||||||
|
>
|
||||||
|
<!-- Top-right Icon -->
|
||||||
|
<div
|
||||||
|
class="absolute top-4 right-4 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
||||||
|
>
|
||||||
|
<v-icon large>mdi-pencil</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||||
|
<template #default="{ close }">
|
||||||
|
<div class="bg-white rounded-2xl p-4">
|
||||||
|
<banner-editor :creator="brandingStore.value"
|
||||||
|
@closeRequested="() => isDialogOpen = false"
|
||||||
|
></banner-editor>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BannerEditor from "@/views/creators/BannerEditor.vue";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||||
|
import {useAuthStore} from "@/stores/authStore.js";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const brandingStore = useBrandingStore();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const showTint = ref(false);
|
||||||
|
const isDialogOpen = ref(false);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const openBannerEditor = () => {
|
||||||
|
isDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentCreator = computed(() => {
|
||||||
|
return authStore.userId === brandingStore.value.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
25
frontend/src/views/creators/Banner.vue
Normal file
25
frontend/src/views/creators/Banner.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<!-- PC -->
|
||||||
|
<div class="shadow-lg rounded-2xl mt-2">
|
||||||
|
<div class="relative z-20">
|
||||||
|
|
||||||
|
<div class="min-h-8 rounded-t-2xl shadow-lg"
|
||||||
|
:style="{ backgroundColor: branding.colors.primary }"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<actual-banner></actual-banner>
|
||||||
|
<banner-actions></banner-actions>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||||
|
import ActualBanner from "@/views/creators/ActualBanner.vue";
|
||||||
|
import BannerActions from "@/views/creators/BannerActions.vue";
|
||||||
|
|
||||||
|
const branding = useBrandingStore();
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useClient } from '@/plugins/api.js';
|
import {useClient} from '@/plugins/api.js';
|
||||||
import { useBrandingStore } from '@/stores/brandingStore.js';
|
import {useBrandingStore} from '@/stores/brandingStore.js';
|
||||||
import DonationButtonBanner from '@/views/creators/DonationButtonBanner.vue';
|
import DonationButtonBanner from '@/views/creators/DonationButtonBanner.vue';
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import {onBeforeUnmount, onMounted, ref} from 'vue';
|
||||||
import IconAccountVerified from "@/components/icons/IconAccountVerified.vue";
|
import CreatorLogo from "@/views/creators/CreatorLogo.vue";
|
||||||
|
import NameTitle from "@/views/creators/NameTitle.vue";
|
||||||
|
|
||||||
const brandingStore = useBrandingStore();
|
const brandingStore = useBrandingStore();
|
||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
@@ -80,10 +81,10 @@ onMounted(async () => {
|
|||||||
window.addEventListener('resize', updateIsMobile);
|
window.addEventListener('resize', updateIsMobile);
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
isSticky.value = !entry.isIntersecting;
|
isSticky.value = !entry.isIntersecting;
|
||||||
},
|
},
|
||||||
{ threshold: 0 }
|
{threshold: 0}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mainContainer.value) {
|
if (mainContainer.value) {
|
||||||
@@ -109,117 +110,48 @@ onBeforeUnmount(() => {
|
|||||||
<div class="flex flex-column w-full">
|
<div class="flex flex-column w-full">
|
||||||
<!-- Container principal avec le profil -->
|
<!-- Container principal avec le profil -->
|
||||||
<div class="relative w-full shadow-xl rounded-2xl">
|
<div class="relative w-full shadow-xl rounded-2xl">
|
||||||
<div
|
<div class="rounded-b-2xl shadow-2xl"
|
||||||
ref="mainContainer"
|
:style="{
|
||||||
class="rounded-b-2xl shadow-2xl"
|
backgroundColor: brandingStore.colors.primary,
|
||||||
:style="{
|
boxShadow: '0 5px 10px rgba(0, 0, 0, 0.3)',
|
||||||
backgroundColor: brandingStore.colors.primary,
|
}">
|
||||||
boxShadow: '0 5px 10px rgba(0, 0, 0, 0.3)',
|
|
||||||
}"
|
<div class="flex flex-row p-2">
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<!-- Profile et Info -->
|
<!-- Profile et Info -->
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Version PC -->
|
<creator-logo/>
|
||||||
<div v-show="!isMobile" class="items-start">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
class="shadow-2xl rounded-full border-solid border-102 absolute z-20 max-w-[190px] ml-10 -mt-5"
|
|
||||||
:src="
|
|
||||||
brandingStore.value.images.logo
|
|
||||||
? brandingStore.value.images.logo
|
|
||||||
: '/images/placeholders/logo.png'
|
|
||||||
"
|
|
||||||
alt="Profile Picture"
|
|
||||||
:style="{
|
|
||||||
borderColor: brandingStore.colors.secondary,
|
|
||||||
height: '190px',
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="ml-64 w-25 min-w-60 flex flex-row"
|
|
||||||
:style="{ color: brandingStore.colors.onPrimary }"
|
|
||||||
>
|
|
||||||
<div v-show="brandingStore.value.verified" class="text-blue m-4 align-content-center verifiedhook">
|
|
||||||
<icon-account-verified></icon-account-verified>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<span class="capitalize text-3xl titlepos">
|
|
||||||
{{ brandingStore.value.name }}
|
|
||||||
</span>
|
|
||||||
<span class="capitalize text-lg titlepos">
|
|
||||||
{{ brandingStore.value.title }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Version Mobile -->
|
|
||||||
<div class="relative">
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
borderColor: brandingStore.colors.secondary,
|
|
||||||
height: '80px',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-show="isMobile"
|
|
||||||
class="absolute -top-7 left-0 px-3 flex flex-row items-center z-30"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
class="shadow-2xl rounded-full border-solid z-20 max-w-[150px]"
|
|
||||||
:src="
|
|
||||||
brandingStore.value.images.logo
|
|
||||||
? brandingStore.value.images.logo
|
|
||||||
: '/images/placeholders/logo.png'
|
|
||||||
"
|
|
||||||
alt="Profile Picture"
|
|
||||||
:style="{ height: '135px' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-show="brandingStore.value.verified" class="text-blue m-4 align-content-center">
|
|
||||||
<icon-account-verified></icon-account-verified>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 text-white w-full flex flex-col items-start">
|
|
||||||
<p class="capitalize text-2xl">
|
|
||||||
{{ brandingStore.value.name }}
|
|
||||||
</p>
|
|
||||||
<p class="capitalize text-md">
|
|
||||||
{{ brandingStore.value.title }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<name-title></name-title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Actions - Follow et Register -->
|
<!-- Actions - Follow et Register -->
|
||||||
<!-- <div class="flex flex-col items-center justify-center w-full">-->
|
<!-- <div class="flex flex-col items-center justify-center w-full">-->
|
||||||
<!-- <div class="flex flex-row space-x-1 justify-center mt-3 mb-2">-->
|
<!-- <div class="flex flex-row space-x-1 justify-center mt-3 mb-2">-->
|
||||||
<!-- <!–<subscribe-button></subscribe-button>–>-->
|
<!-- <!–<subscribe-button></subscribe-button>–>-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
<!-- </div>-->
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bouton Support -->
|
<!-- Bouton Support -->
|
||||||
<div
|
<div
|
||||||
v-show="brandingStore.value.acceptDonation"
|
v-show="brandingStore.value.acceptDonation"
|
||||||
class="z-20 shadow-2xl rounded-md text-white flex justify-center items-center z-50"
|
class="z-20 shadow-2xl rounded-md text-white flex justify-center items-center z-50"
|
||||||
:class="{
|
:class="{
|
||||||
'absolute bottom-6 right-8 w-64 h-28 ': !isMobile,
|
'absolute bottom-6 right-8 w-64 h-28 ': !isMobile,
|
||||||
'fixed bottom-0 left-0 right-0 w-full h-16': isMobile,
|
'fixed bottom-0 left-0 right-0 w-full h-16': isMobile,
|
||||||
}"
|
}"
|
||||||
:style="{ backgroundColor: brandingStore.colors.secondary }"
|
:style="{ backgroundColor: brandingStore.colors.secondary }"
|
||||||
>
|
>
|
||||||
<donation-button-banner
|
<donation-button-banner
|
||||||
v-if="creator"
|
v-if="creator"
|
||||||
:creator-id="creator.id"
|
:creator-id="creator.id"
|
||||||
:creator-name="creator.name"
|
:creator-name="creator.name"
|
||||||
:on-success-url="baseURL + '/paymentcompleted/' + creator.id"
|
:on-success-url="baseURL + '/paymentcompleted/' + creator.id"
|
||||||
:on-cancelled-url="baseURL + '/paymentfailed/' + creator.id"
|
:on-cancelled-url="baseURL + '/paymentfailed/' + creator.id"
|
||||||
></donation-button-banner>
|
></donation-button-banner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,8 +159,8 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<!-- Section pour les icônes de réseaux sociaux -->
|
<!-- Section pour les icônes de réseaux sociaux -->
|
||||||
<div
|
<div
|
||||||
class="rounded-b-2xl -mt-3 h-12 px-36 flex flex-col items-center justify-center"
|
class="rounded-b-2xl -mt-3 h-12 px-36 flex flex-col items-center justify-center"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: brandingStore.colors.secondary,
|
backgroundColor: brandingStore.colors.secondary,
|
||||||
boxShadow: '0 5px 20px rgba(0, 0, 0, 0.3)',
|
boxShadow: '0 5px 20px rgba(0, 0, 0, 0.3)',
|
||||||
}"
|
}"
|
||||||
@@ -236,20 +168,20 @@ onBeforeUnmount(() => {
|
|||||||
<div class="flex justify-evenly mt-3 w-full">
|
<div class="flex justify-evenly mt-3 w-full">
|
||||||
<div class="flex flex-row space-x-6 justify-center">
|
<div class="flex flex-row space-x-6 justify-center">
|
||||||
<a
|
<a
|
||||||
v-for="socialNetwork in GetSocialsUrls()"
|
v-for="socialNetwork in GetSocialsUrls()"
|
||||||
:key="socialNetwork.url"
|
:key="socialNetwork.url"
|
||||||
:href="socialNetwork.url"
|
:href="socialNetwork.url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-white text-md transform transition-transform duration-200 hover:scale-125 hover:text-blue-500"
|
class="text-white text-md transform transition-transform duration-200 hover:scale-125 hover:text-blue-500"
|
||||||
>
|
>
|
||||||
<v-icon v-if="socialNetwork.icon.includes('mdi')">
|
<v-icon v-if="socialNetwork.icon.includes('mdi')">
|
||||||
{{ socialNetwork.icon }}
|
{{ socialNetwork.icon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
:src="socialNetwork.icon"
|
:src="socialNetwork.icon"
|
||||||
class="w-6 h-6 mt-0.5"
|
class="w-6 h-6 mt-0.5"
|
||||||
:alt="socialNetwork.url"
|
:alt="socialNetwork.url"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,28 +190,3 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.nav-button {
|
|
||||||
@apply rounded flex justify-center font-sans py-1 text-white tracking-widest p-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-button:hover {
|
|
||||||
@apply bg-purple-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition CSS */
|
|
||||||
.transition-all {
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.titlepos {
|
|
||||||
position: relative;
|
|
||||||
top: 30px;
|
|
||||||
}
|
|
||||||
.verifiedhook{
|
|
||||||
position: relative;
|
|
||||||
top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<h2 class="text-2xl font-semibold mb-4 flex justify-center">
|
<h2 class="text-2xl font-semibold mb-4">
|
||||||
Bannière
|
Bannière
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import {ref} from 'vue'
|
||||||
import { useClient } from '@/plugins/api.js'
|
import {useClient} from '@/plugins/api.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
@@ -36,7 +36,7 @@ const props = defineProps({
|
|||||||
const emits = defineEmits(['closeRequested'])
|
const emits = defineEmits(['closeRequested'])
|
||||||
|
|
||||||
const selectedFile = ref({})
|
const selectedFile = ref({})
|
||||||
const fileUrl = ref(props.creator.images.banner)
|
const fileUrl = ref(props.creator?.images?.banner)
|
||||||
const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png'
|
const fallbackUrl = '/images/hutopymedia/banners/hutopyul.png'
|
||||||
|
|
||||||
const onFileSelected = () => {
|
const onFileSelected = () => {
|
||||||
@@ -1,27 +1,37 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import {computed, ref, watch} from 'vue'
|
||||||
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
import {useUserProfileStore} from "@/stores/userProfileStore.js";
|
||||||
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
|
import {useCreatorProfileStore} from "@/stores/creatorProfileStore.js";
|
||||||
import {useClient} from "@/plugins/api.js";
|
import {useClient} from "@/plugins/api.js";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
|
import NameEditor from "@/views/creators/NameEditor.vue";
|
||||||
|
|
||||||
const creatorName = ref('');
|
const creatorName = ref('');
|
||||||
|
const creatorNameReservationId = ref(undefined);
|
||||||
|
const canSave = computed(() => creatorNameReservationId.value !== undefined)
|
||||||
|
|
||||||
|
const isOperationPending = ref(false);
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
const isLoading = ref(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const creatorProfileStore = useCreatorProfileStore();
|
const creatorProfileStore = useCreatorProfileStore();
|
||||||
const userProfileStore = useUserProfileStore();
|
const userProfileStore = useUserProfileStore();
|
||||||
|
|
||||||
|
function handleCreatorNameReservationIdChanged($event) {
|
||||||
|
console.log(`in handleCreatorNameReservationIdChanged: ${$event.value}`);
|
||||||
|
creatorNameReservationId.value = $event.value
|
||||||
|
}
|
||||||
|
|
||||||
async function createAccount() {
|
async function createAccount() {
|
||||||
const client = useClient();
|
|
||||||
try {
|
try {
|
||||||
|
isOperationPending.value = true;
|
||||||
|
const client = useClient();
|
||||||
errorMessage.value = '';
|
errorMessage.value = '';
|
||||||
isLoading.value = true;
|
|
||||||
const normalizedCreatorName = creatorName.value.toLowerCase();
|
const normalizedCreatorName = creatorName.value.toLowerCase();
|
||||||
await client.post('/api/creators', {
|
await client.post('/api/creators', {
|
||||||
creatorId: userProfileStore.user.id,
|
creatorId: userProfileStore.user.id,
|
||||||
name: normalizedCreatorName,
|
name: normalizedCreatorName,
|
||||||
|
slugReservationId: creatorNameReservationId.value,
|
||||||
});
|
});
|
||||||
await creatorProfileStore.fetchCurrentCreatorProfile();
|
await creatorProfileStore.fetchCurrentCreatorProfile();
|
||||||
await router.push(`/@${normalizedCreatorName}`);
|
await router.push(`/@${normalizedCreatorName}`);
|
||||||
@@ -32,38 +42,37 @@ async function createAccount() {
|
|||||||
errorMessage.value = error?.response?.data?.message || error.message || 'An unexpected error occurred.';
|
errorMessage.value = error?.response?.data?.message || error.message || 'An unexpected error occurred.';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isOperationPending.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="create-creator-card">
|
||||||
<div class="create-creator-card">
|
|
||||||
<div class="py-2 text-3xl font-bold">
|
<div class="py-2 text-3xl font-bold text-center mb-10">
|
||||||
<div class="text-center mb-10">Créez votre Hutopy.</div>
|
Créez votre Hutopy.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-column justify-end gap-2">
|
<div class="flex flex-column justify-end gap-2">
|
||||||
<v-alert
|
<v-alert
|
||||||
v-if="!!errorMessage"
|
v-if="!!errorMessage"
|
||||||
dense
|
|
||||||
outlined
|
outlined
|
||||||
text
|
|
||||||
type="error"
|
type="error"
|
||||||
>
|
>
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<v-text-field
|
<name-editor
|
||||||
variant="outlined"
|
v-model:name="creatorName"
|
||||||
v-model="creatorName"
|
creator-name-reservation-id="creatorNameDirty"
|
||||||
label="Nom de la page"
|
@update:creator-name-reservation-id="handleCreatorNameReservationIdChanged($event)"
|
||||||
outlined
|
></name-editor>
|
||||||
></v-text-field>
|
|
||||||
<div class="flex flex-row justify-end gap-2">
|
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end gap-2">
|
||||||
<v-btn
|
<v-btn
|
||||||
:disabled="isLoading"
|
:disabled="!canSave"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@click="createAccount"
|
@click="createAccount"
|
||||||
:style="{ borderColor: 'rgb(159, 76, 173)', color: 'rgb(159, 76, 173)' }"
|
:style="{ borderColor: 'rgb(159, 76, 173)', color: 'rgb(159, 76, 173)' }"
|
||||||
@@ -71,9 +80,9 @@ async function createAccount() {
|
|||||||
Créer
|
Créer
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- PC -->
|
|
||||||
<div v-if="!isMobile">
|
|
||||||
<div class="shadow-lg rounded-2xl mt-2">
|
|
||||||
<div class="relative z-20">
|
|
||||||
<div class="min-h-8 rounded-t-2xl shadow-lg" :style="{ backgroundColor: branding.colors.primary }"></div>
|
|
||||||
<!-- Banner -->
|
|
||||||
<div class="relative">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
class="w-full drop-shadow-[0_10px_6px_rgba(0,0,0,0.25)]"
|
|
||||||
:src="branding.value.images.banner ? branding.value.images.banner : '/images/placeholders/banner.png'"
|
|
||||||
alt="Profile Banner"
|
|
||||||
style="max-height: 425px"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<banner-actions></banner-actions>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile -->
|
|
||||||
<div v-if="isMobile">
|
|
||||||
<div class="shadow-lg rounded-2xl ">
|
|
||||||
<div class="relative z-20">
|
|
||||||
<div class="shadow-2xl flex items-center px-2 py-2"
|
|
||||||
:style="{ backgroundColor: branding.colors.primary, color: branding.colors.onPrimary }">
|
|
||||||
|
|
||||||
<router-link to="/@Hutopy">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<HutopySvg></HutopySvg>
|
|
||||||
<div class="text-xl font-bold -ml-2 ">Hutopy</div>
|
|
||||||
</div>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
|
||||||
|
|
||||||
<router-link to="/login">
|
|
||||||
<button class="lg:hidden flex items-center justify-center mr-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
||||||
:stroke="branding.colors.onPrimary" class="w-8 h-8">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Banner -->
|
|
||||||
<div class="relative">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
class="w-full drop-shadow-[0_10px_6px_rgba(0,0,0,0.25)]"
|
|
||||||
:src="branding.value.images.banner ? branding.value.images.banner : '/images/placeholders/banner.png'"
|
|
||||||
alt="Profile Banner"
|
|
||||||
style="max-height: 425px"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<banner-actions></banner-actions>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {ref, onMounted, onBeforeUnmount} from "vue";
|
|
||||||
import BannerActions from "@/views/creators/BannerActions.vue";
|
|
||||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
|
||||||
import HutopySvg from "@/views/svg/HutopySvg.vue";
|
|
||||||
|
|
||||||
|
|
||||||
const branding = useBrandingStore();
|
|
||||||
const isMobile = ref(false);
|
|
||||||
|
|
||||||
function updateIsMobile() {
|
|
||||||
isMobile.value = window.innerWidth <= 640;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateIsMobile();
|
|
||||||
window.addEventListener("resize", updateIsMobile);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener("resize", updateIsMobile);
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<v-progress-linear indeterminate></v-progress-linear>
|
<v-progress-linear indeterminate></v-progress-linear>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<creator-banner></creator-banner>
|
<banner></banner>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-8 flex-grow">
|
<div class="py-8 flex-grow">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script async setup>
|
<script async setup>
|
||||||
import CreatorBanner from "@/views/creators/CreatorBanner.vue";
|
import Banner from "@/views/creators/Banner.vue";
|
||||||
import Footer from "@/views/main/Footer.vue";
|
import Footer from "@/views/main/Footer.vue";
|
||||||
import {useBrandingStore} from "@/stores/brandingStore.js";
|
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||||
const brandingStore = useBrandingStore()
|
const brandingStore = useBrandingStore()
|
||||||
|
|||||||
70
frontend/src/views/creators/CreatorLogo.vue
Normal file
70
frontend/src/views/creators/CreatorLogo.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="rounded-full relative bg-red"
|
||||||
|
@mouseenter="showTint = isCurrentCreator"
|
||||||
|
@mouseleave="showTint = false"
|
||||||
|
@click="isCurrentCreator && openBannerEditor()"
|
||||||
|
>
|
||||||
|
|
||||||
|
<img
|
||||||
|
class="shadow-2xl rounded-full border-solid border-102 max-w-[190px]"
|
||||||
|
:src="brandingStore.value.images.logo
|
||||||
|
? brandingStore.value.images.logo
|
||||||
|
: '/images/placeholders/logo.png'"
|
||||||
|
alt="Profile Picture"
|
||||||
|
:style="{
|
||||||
|
borderColor: brandingStore.colors.secondary,
|
||||||
|
height: '190px',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Tint Effect -->
|
||||||
|
<div
|
||||||
|
v-if="showTint"
|
||||||
|
class="absolute rounded-full inset-0 bg-black/25 cursor-pointer"
|
||||||
|
>
|
||||||
|
<!-- Top-right Icon -->
|
||||||
|
<div
|
||||||
|
class="absolute top-4 right-4 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
||||||
|
>
|
||||||
|
<v-icon large>mdi-pencil</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||||
|
<template #default="{ close }">
|
||||||
|
<div class="bg-white rounded-2xl p-4">
|
||||||
|
<creator-logo-editor
|
||||||
|
:creator="brandingStore?.value"
|
||||||
|
@closeRequested="() => isDialogOpen = false"
|
||||||
|
></creator-logo-editor>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useAuthStore} from "@/stores/authStore.js";
|
||||||
|
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||||
|
import CreatorLogoEditor from "@/views/creators/CreatorLogoEditor.vue";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const brandingStore = useBrandingStore();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const showTint = ref(false);
|
||||||
|
const isDialogOpen = ref(false);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const openBannerEditor = () => {
|
||||||
|
isDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentCreator = computed(() => {
|
||||||
|
return authStore.userId === brandingStore.value.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -23,12 +23,13 @@
|
|||||||
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
<v-btn color="black" variant="text" @click="cancel">Annuler</v-btn>
|
||||||
<v-btn color="#A6147D" @click="publish">Enregistrer</v-btn>
|
<v-btn color="#A6147D" @click="publish">Enregistrer</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import {ref} from 'vue'
|
||||||
import { useClient } from '@/plugins/api.js'
|
import {useClient} from '@/plugins/api.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
89
frontend/src/views/creators/NameEditor.vue
Normal file
89
frontend/src/views/creators/NameEditor.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {v7} from "uuid";
|
||||||
|
import {useClient} from "@/plugins/api.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
creatorNameReservationId: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits([
|
||||||
|
'update:name',
|
||||||
|
'update:creatorNameReservationId'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const name = ref(props.name);
|
||||||
|
const isReserved = computed(() => reservationState.value === 'reserved');
|
||||||
|
|
||||||
|
const isOperationPending = ref(false);
|
||||||
|
const reservationState = ref(null);
|
||||||
|
const reservationId = ref(null);
|
||||||
|
|
||||||
|
let timeout = null;
|
||||||
|
const handleInput = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() =>
|
||||||
|
checkNameAvailability(),
|
||||||
|
200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = useClient()
|
||||||
|
const checkNameAvailability = async () => {
|
||||||
|
if (!name.value || name.value.trim() === "") {
|
||||||
|
reservationState.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const id = v7();
|
||||||
|
isOperationPending.value = true;
|
||||||
|
reservationState.value = "loading";
|
||||||
|
await client.post(
|
||||||
|
`/api/creators/@${encodeURIComponent(name.value)}/reserve`,
|
||||||
|
{reservationId: id}
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
isOperationPending.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-text-field
|
||||||
|
variant="outlined"
|
||||||
|
label="Nom de la page"
|
||||||
|
v-model="name"
|
||||||
|
outlined
|
||||||
|
@input="handleInput"
|
||||||
|
>
|
||||||
|
<template #append-inner>
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="reservationState === 'loading'"
|
||||||
|
indeterminate
|
||||||
|
size="24"
|
||||||
|
width="3"
|
||||||
|
color="grey"
|
||||||
|
></v-progress-circular>
|
||||||
|
|
||||||
|
<v-icon v-else-if="reservationState === 'reserved'" color="green">mdi-check-circle</v-icon>
|
||||||
|
<v-icon v-else-if="reservationState === 'unavailable'" color="red">mdi-close-circle</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-text-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
79
frontend/src/views/creators/NameTitle.vue
Normal file
79
frontend/src/views/creators/NameTitle.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
|
||||||
|
<div class="relative flex flex-row"
|
||||||
|
@mouseenter="showTint = isCurrentCreator"
|
||||||
|
@mouseleave="showTint = false"
|
||||||
|
@click="isCurrentCreator && openBannerEditor()"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div v-show="brandingStore.value.verified"
|
||||||
|
class="text-blue m-4">
|
||||||
|
<icon-account-verified></icon-account-verified>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col"
|
||||||
|
:style="{ color: brandingStore.colors.onPrimary }">
|
||||||
|
<span class="capitalize text-3xl">
|
||||||
|
{{ brandingStore.value.name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="capitalize text-lg">
|
||||||
|
{{ brandingStore.value.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tint Effect -->
|
||||||
|
<div
|
||||||
|
v-if="showTint"
|
||||||
|
class="absolute inset-0 bg-black/25 cursor-pointer"
|
||||||
|
>
|
||||||
|
<!-- Top-right Icon -->
|
||||||
|
<div
|
||||||
|
class="absolute top-1 right-1 w-12 h-12 bg-white rounded-full flex items-center justify-center shadow-lg"
|
||||||
|
>
|
||||||
|
<v-icon large>mdi-pencil</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-dialog v-model="isDialogOpen" max-width="800px">
|
||||||
|
<template #default="{ close }">
|
||||||
|
<div class="bg-white rounded-2xl p-4">
|
||||||
|
<name-title-editor
|
||||||
|
:creator="brandingStore?.value"
|
||||||
|
@closeRequested="() => isDialogOpen = false"
|
||||||
|
></name-title-editor>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import IconAccountVerified from "@/components/icons/IconAccountVerified.vue";
|
||||||
|
import {useBrandingStore} from "@/stores/brandingStore.js";
|
||||||
|
import {useAuthStore} from "@/stores/authStore.js";
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import NameTitleEditor from "@/views/creators/NameTitleEditor.vue";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const brandingStore = useBrandingStore();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const showTint = ref(false);
|
||||||
|
const isDialogOpen = ref(false);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const openBannerEditor = () => {
|
||||||
|
isDialogOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentCreator = computed(() => {
|
||||||
|
return authStore.userId === brandingStore.value.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
76
frontend/src/views/creators/NameTitleEditor.vue
Normal file
76
frontend/src/views/creators/NameTitleEditor.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {useClient} from "@/plugins/api.js";
|
||||||
|
import NameTitle from "@/views/creators/NameTitle.vue";
|
||||||
|
import NameEditor from "@/views/creators/NameEditor.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
creator: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['closeRequested'])
|
||||||
|
|
||||||
|
const name = ref(props.creator.name);
|
||||||
|
const title = ref(props.creator.title);
|
||||||
|
|
||||||
|
const canSave = computed(() => name != props.creator.name);
|
||||||
|
|
||||||
|
const client = useClient()
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
await client.post(`/api/creators/${props.creator.id}/name`);
|
||||||
|
await client.post(`/api/creators/${props.creator.id}/title`);
|
||||||
|
props.creator.creator.name = name;
|
||||||
|
props.creator.title.name = title;
|
||||||
|
emits('closeRequested')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emits('closeRequested');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pb-5 text-2xl">
|
||||||
|
Modifier le Titre
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<name-editor
|
||||||
|
:name="name"
|
||||||
|
></name-editor>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
variant="outlined"
|
||||||
|
v-model="title"
|
||||||
|
label="Titre"
|
||||||
|
outlined
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<div class="flex justify-end space-x-4">
|
||||||
|
|
||||||
|
<v-btn color="black"
|
||||||
|
variant="text"
|
||||||
|
@click="cancel">
|
||||||
|
Annuler
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn color="#A6147D"
|
||||||
|
:disabled="!canSave"
|
||||||
|
@click="save">
|
||||||
|
Enregistrer
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,95 +1,89 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center w-[800px] gap-4">
|
|
||||||
|
|
||||||
<h1 class="uppercase pb-5 text-2xl">
|
<v-card rounded="xl" class="w-full">
|
||||||
<v-icon class="mr-2">mdi-information</v-icon>
|
|
||||||
{{ $t('personnalinformation.title') }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<v-card class="w-full">
|
<v-card-title>
|
||||||
<v-card-title>
|
{{ $t('personnalinformation.informations') }}
|
||||||
{{ $t('personnalinformation.informations') }}
|
</v-card-title>
|
||||||
</v-card-title>
|
|
||||||
|
|
||||||
<!-- <button-->
|
<!-- <button-->
|
||||||
<!-- class="editableValue"-->
|
<!-- class="editableValue"-->
|
||||||
<!-- @click="openEditPortrait">-->
|
<!-- @click="openEditPortrait">-->
|
||||||
<!-- <span class="label">{{ $t('personnalinformation.profilepicture') }}</span>-->
|
<!-- <span class="label">{{ $t('personnalinformation.profilepicture') }}</span>-->
|
||||||
<!-- <span class="value">Un portrait vous permet de personnaliser votre profil</span>-->
|
<!-- <span class="value">Un portrait vous permet de personnaliser votre profil</span>-->
|
||||||
<!-- <span>-->
|
<!-- <span>-->
|
||||||
<!-- <img-->
|
<!-- <img-->
|
||||||
<!-- :src="userProfileStore.user.portraitUrl"-->
|
<!-- :src="userProfileStore.user.portraitUrl"-->
|
||||||
<!-- alt="Profile Image"-->
|
<!-- alt="Profile Image"-->
|
||||||
<!-- class="rounded-full"-->
|
<!-- class="rounded-full"-->
|
||||||
<!-- width="48px"-->
|
<!-- width="48px"-->
|
||||||
<!-- height="48px"/>-->
|
<!-- height="48px"/>-->
|
||||||
<!-- </span>-->
|
<!-- </span>-->
|
||||||
<!-- </button>-->
|
<!-- </button>-->
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="editableValue"
|
class="editableValue"
|
||||||
@click="openEditFullname">
|
@click="openEditFullname">
|
||||||
<span class="label">{{ $t('personnalinformation.fullname') }}</span>
|
<span class="label">{{ $t('personnalinformation.fullname') }}</span>
|
||||||
<span class="value">{{ userProfileStore.fullname }}</span>
|
<span class="value">{{ userProfileStore.fullname }}</span>
|
||||||
<span><v-icon>mdi-chevron-right</v-icon></span>
|
<span><v-icon>mdi-chevron-right</v-icon></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="editableValue"
|
class="editableValue"
|
||||||
@click="openEditAlias">
|
@click="openEditAlias">
|
||||||
<span class="label">{{ $t('personnalinformation.alias') }}</span>
|
<span class="label">{{ $t('personnalinformation.alias') }}</span>
|
||||||
<span class="value">{{ userProfileStore.user.alias }}</span>
|
<span class="value">{{ userProfileStore.user.alias }}</span>
|
||||||
<span><v-icon>mdi-chevron-right</v-icon></span>
|
<span><v-icon>mdi-chevron-right</v-icon></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- <button-->
|
<!-- <button-->
|
||||||
<!-- class="editableValue"-->
|
<!-- class="editableValue"-->
|
||||||
<!-- @click="openEditBirthday">-->
|
<!-- @click="openEditBirthday">-->
|
||||||
<!-- <span class="label">{{ $t('personnalinformation.dob') }}</span>-->
|
<!-- <span class="label">{{ $t('personnalinformation.dob') }}</span>-->
|
||||||
<!-- <span class="value">{{ userProfileStore.user.birthDate }}</span>-->
|
<!-- <span class="value">{{ userProfileStore.user.birthDate }}</span>-->
|
||||||
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
||||||
<!-- </button>-->
|
<!-- </button>-->
|
||||||
|
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- Phone & email -->
|
<!-- Phone & email -->
|
||||||
<v-card class="w-full">
|
<v-card rounded="xl" class="w-full">
|
||||||
<v-card-title>
|
<v-card-title>
|
||||||
{{ $t('personnalinformation.contactdetails') }}
|
{{ $t('personnalinformation.contactdetails') }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="editableValue"
|
class="editableValue"
|
||||||
@click="openEditEmail">
|
@click="openEditEmail">
|
||||||
<span class="label">{{ $t('personnalinformation.email') }}</span>
|
<span class="label">{{ $t('personnalinformation.email') }}</span>
|
||||||
<span class="value">{{ userProfileStore.user.email }}</span>
|
<span class="value">{{ userProfileStore.user.email }}</span>
|
||||||
<span><v-icon>mdi-chevron-right</v-icon></span>
|
<span><v-icon>mdi-chevron-right</v-icon></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- <button-->
|
<!-- <button-->
|
||||||
<!-- class="editableValue"-->
|
<!-- class="editableValue"-->
|
||||||
<!-- @click="openEditPhone">-->
|
<!-- @click="openEditPhone">-->
|
||||||
<!-- <span class="label">{{ $t('personnalinformation.phone') }}</span>-->
|
<!-- <span class="label">{{ $t('personnalinformation.phone') }}</span>-->
|
||||||
<!-- <span class="value">{{ userProfileStore.user.phoneNumber }}</span>-->
|
<!-- <span class="value">{{ userProfileStore.user.phoneNumber }}</span>-->
|
||||||
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
||||||
<!-- </button>-->
|
<!-- </button>-->
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- Address -->
|
<!-- Address -->
|
||||||
<!-- <v-card class="w-full">-->
|
<!-- <v-card class="w-full">-->
|
||||||
<!-- <v-card-title>-->
|
<!-- <v-card-title>-->
|
||||||
<!-- {{ $t('personnalinformation.addresses') }}-->
|
<!-- {{ $t('personnalinformation.addresses') }}-->
|
||||||
<!-- </v-card-title>-->
|
<!-- </v-card-title>-->
|
||||||
|
|
||||||
<!-- <button-->
|
<!-- <button-->
|
||||||
<!-- class="editableValue"-->
|
<!-- class="editableValue"-->
|
||||||
<!-- @click="openEditAddress">-->
|
<!-- @click="openEditAddress">-->
|
||||||
<!-- <span class="label">{{ $t('personnalinformation.home') }}</span>-->
|
<!-- <span class="label">{{ $t('personnalinformation.home') }}</span>-->
|
||||||
<!-- <span class="value">{{ userProfileStore.user.address }}</span>-->
|
<!-- <span class="value">{{ userProfileStore.user.address }}</span>-->
|
||||||
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
<!-- <span><v-icon>mdi-chevron-right</v-icon></span>-->
|
||||||
<!-- </button>-->
|
<!-- </button>-->
|
||||||
<!-- </v-card>-->
|
<!-- </v-card>-->
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<v-dialog v-model="dialogEditPortraitShown" max-width="600px">
|
<v-dialog v-model="dialogEditPortraitShown" max-width="600px">
|
||||||
@@ -152,8 +146,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
import AddressDialog from './AddressDialog.vue';
|
import AddressDialog from './account/AddressDialog.vue';
|
||||||
import EmailDialog from "./EmailDialog.vue";
|
import EmailDialog from "./account/EmailDialog.vue";
|
||||||
import PhoneDialog from "@/views/profile/account/PhoneDialog.vue";
|
import PhoneDialog from "@/views/profile/account/PhoneDialog.vue";
|
||||||
import BirthdayDialog from "@/views/profile/account/BirthdayDialog.vue";
|
import BirthdayDialog from "@/views/profile/account/BirthdayDialog.vue";
|
||||||
import AliasDialog from "@/views/profile/account/AliasDialog.vue";
|
import AliasDialog from "@/views/profile/account/AliasDialog.vue";
|
||||||
283
frontend/src/views/profile/CreatorPage.vue
Normal file
283
frontend/src/views/profile/CreatorPage.vue
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<script setup>
|
||||||
|
import XIcon from '@/assets/icons/x.svg';
|
||||||
|
import {useCreatorProfileStore} from '@/stores/creatorProfileStore.js';
|
||||||
|
import ChangeStripeID from '@/views/profile/creators/ChangeStripeID.vue';
|
||||||
|
import ChangeTitle from '@/views/profile/creators/ChangeTitle.vue';
|
||||||
|
import {computed, ref} from 'vue';
|
||||||
|
import ColorsPicker from './creators/ColorsPicker.vue';
|
||||||
|
import LogoPicker from '../creators/CreatorLogoEditor.vue';
|
||||||
|
import Socials from './creators/Socials.vue';
|
||||||
|
|
||||||
|
const creatorProfileStore = useCreatorProfileStore();
|
||||||
|
|
||||||
|
const dialog = ref(false);
|
||||||
|
const currentComponent = ref('');
|
||||||
|
|
||||||
|
const componentsMap = {
|
||||||
|
LogoPicker,
|
||||||
|
Socials,
|
||||||
|
ColorsPicker,
|
||||||
|
ChangeTitle,
|
||||||
|
ChangeStripeID,
|
||||||
|
};
|
||||||
|
|
||||||
|
function requestCancel() {
|
||||||
|
currentComponent.value = null;
|
||||||
|
dialog.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDialog = (component) => {
|
||||||
|
currentComponent.value = componentsMap[component];
|
||||||
|
dialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
currentComponent.value = null;
|
||||||
|
dialog.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog" max-width="800px">
|
||||||
|
<v-card
|
||||||
|
:style="{ borderRadius: '25px', border: '3px solid rgb(159, 76, 173)' }"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<component
|
||||||
|
:is="currentComponent"
|
||||||
|
:creator="creatorProfileStore.creator"
|
||||||
|
@closeRequested="closeDialog"
|
||||||
|
@requestAccept="requestAccept"
|
||||||
|
@requestCancel="requestCancel"
|
||||||
|
></component>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Lorsque l'utilisateur n'a pas de creator name-->
|
||||||
|
<v-card rounded="xl" class="w-full">>
|
||||||
|
<h1 class="uppercase">
|
||||||
|
{{ $t('creatorinfopage.informations') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- INFOS -->
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<button
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="flex-none pa-2 min-w-32 text-left">
|
||||||
|
{{ $t('creatorinfopage.name') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex-auto text-left pr-6 capitalize">
|
||||||
|
{{ creatorProfileStore.creator.name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TITLE -->
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<button
|
||||||
|
@click="openDialog('ChangeTitle')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="flex-none pa-2 min-w-32 text-left">{{
|
||||||
|
$t('creatorinfopage.title')
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-auto text-left pr-6 capitalize">{{
|
||||||
|
creatorProfileStore.creator.title
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STRIPE -->
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<button
|
||||||
|
@click="openDialog('ChangeStripeID')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full rounded-b"
|
||||||
|
>
|
||||||
|
<span class="flex-none pa-2 min-w-32 text-left"
|
||||||
|
>Stripe Account ID</span
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.stripeId
|
||||||
|
}}</span>
|
||||||
|
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card rounded="xl" class="w-full">
|
||||||
|
<div class="py-5 uppercase ml-4">
|
||||||
|
{{ $t('creatorinfopage.banner&profile') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<button
|
||||||
|
@click="openDialog('ColorsPicker')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6 capitalize">
|
||||||
|
Choisissez votre palette de couleurs.
|
||||||
|
</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<v-card rounded="xl" class="w-full">
|
||||||
|
<div class="uppercase">
|
||||||
|
{{ $t('creatorinfopage.socialnetwork') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="pa-2 min-w-32 text-left">
|
||||||
|
<v-icon>mdi-facebook</v-icon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex-auto text-left pr-6">
|
||||||
|
{{ creatorProfileStore.creator.socials?.facebookUrl }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="flex-none pa-2 min-w-32 text-left">
|
||||||
|
<v-icon>mdi-instagram</v-icon></span
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.instagramUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="flex-none pa-2 w-9 h-9 text-left ml-0.5">
|
||||||
|
<XIcon></XIcon>
|
||||||
|
</span>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.xUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="pa-2 min-w-32 text-left"
|
||||||
|
><v-icon>mdi-linkedin</v-icon></span
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.linkedInUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="flex-none pa-2 min-w-32 text-left">
|
||||||
|
<XIcon class="w-5 h-5"></XIcon>
|
||||||
|
</span>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.tikTokUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="pa-2 min-w-32 text-left"
|
||||||
|
><v-icon>mdi-youtube</v-icon></span
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.youtubeUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
||||||
|
>
|
||||||
|
<span class="pa-2 min-w-32 text-left"
|
||||||
|
><v-icon>mdi-reddit</v-icon></span
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.redditUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="openDialog('Socials')"
|
||||||
|
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full rounded-b"
|
||||||
|
>
|
||||||
|
<span class="pa-2 min-w-32 text-left"
|
||||||
|
><v-icon>mdi-web</v-icon></span
|
||||||
|
>
|
||||||
|
<span class="flex-auto text-left pr-6">{{
|
||||||
|
creatorProfileStore.creator.socials?.websiteUrl
|
||||||
|
}}</span>
|
||||||
|
<span class="flex-none">
|
||||||
|
<v-icon>mdi-chevron-right</v-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.HoverBtn:hover {
|
||||||
|
@apply bg-[#A6147D] text-white;
|
||||||
|
@apply hover:opacity-90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-border {
|
||||||
|
border: 3px solid;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,147 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Mobile -->
|
|
||||||
<div v-if="isMobileView" class="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6 text-center">
|
|
||||||
<!-- Image -->
|
|
||||||
<img src="/images/usersmedia/HutopyProfile/profilepictures/profileHutopyProfile01.png" alt="Image" class="w-64 h-64 rounded-full mb-4 border" />
|
|
||||||
|
|
||||||
<!-- Message -->
|
<div class="bg-red flex flex-col gap-8 p-8">
|
||||||
<div class="text-lg text-gray-700 mt-8">
|
<account-page></account-page>
|
||||||
<p class="font-semibold mb-2">Pour vous connecter et modifier votre page, veuillez utiliser un appareil avec un écran plus large, comme un ordinateur.</p>
|
<creator-page></creator-page>
|
||||||
<p>Pour le moment, l'expérience sur téléphone n'est pas encore complétée.</p>
|
|
||||||
<p class="mt-4 font-bold">Désolé de l'inconvénient.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PC -->
|
|
||||||
<div v-else>
|
|
||||||
<div class="flex flex-col md:flex-row bg-[#f4f4f4] h-full">
|
|
||||||
<!-- Left Menu -->
|
|
||||||
<div class=" z-20 w-full md:max-w-xs fixed md:sticky md:top-0 md:flex md:flex-col top-0">
|
|
||||||
<div class="sticky top-20 z-30">
|
|
||||||
<div class="flex flex-col items-center md:items-start md:pl-4 mt-16">
|
|
||||||
<h1 class="text-2xl py-4 font-bold text-center md:text-left">{{$t('profilemenu.manageyouraccount')}}</h1>
|
|
||||||
|
|
||||||
<div class="relative flex items-center md:mt-0 w-full">
|
|
||||||
<!-- Navigation buttons for small screens -->
|
|
||||||
<button @click="scrollLeftFunc"
|
|
||||||
class="rounded p-1 absolute left-2 z-10 md:hidden text-fuchsia-800 text-2xl ">
|
|
||||||
<v-icon>mdi-chevron-left</v-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref="scrollContainer"
|
|
||||||
class="flex md:flex-col space-x-2 space-y-0 md:space-x-0 md:space-y-2 p-4 items-center md:items-start overflow-x-scroll md:overflow-x-visible mx-2 md:mx-0 custom-scroll min-w-[400px] px-1"
|
|
||||||
@mousedown="mouseDown"
|
|
||||||
@mouseleave="mouseLeave"
|
|
||||||
@mouseup="mouseUp"
|
|
||||||
@mousemove="mouseMove">
|
|
||||||
|
|
||||||
<v-btn variant="text" @click="currentComponent = 'CreatorPage'">
|
|
||||||
<v-icon class="mr-2">mdi-file-edit-outline</v-icon>
|
|
||||||
{{ $t('profilemenu.creator') }}
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn variant="text" @click="currentComponent = 'AccountPage'">
|
|
||||||
<v-icon class="mr-2">mdi-information</v-icon>
|
|
||||||
{{ $t('profilemenu.user') }}
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<button @click="scrollRightFunc"
|
|
||||||
class="rounded p-1 absolute right-2 z-10 md:hidden text-fuchsia-800 bg-[#f4f4f4] text-2xl">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mid Content -->
|
|
||||||
<div class="flex flex-col flex-1 align-center py-12 p-3 mt-28 md:mt-0">
|
|
||||||
<template v-if="currentComponent === 'CreatorPage'">
|
|
||||||
<creator-page></creator-page>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="currentComponent === 'AccountPage'">
|
|
||||||
<account-page></account-page>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from "vue";
|
import CreatorPage from "@/views/profile/CreatorPage.vue";
|
||||||
import CreatorPage from "@/views/profile/creators/CreatorPage.vue";
|
import AccountPage from "@/views/profile/AccountPage.vue";
|
||||||
import AccountPage from "@/views/profile/account/AccountPage.vue";
|
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import { useDisplay } from "vuetify";
|
|
||||||
|
|
||||||
const { smAndDown } = useDisplay();
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const startingComponent = route.query.target || 'CreatorPage';
|
|
||||||
const currentComponent = ref(startingComponent);
|
|
||||||
|
|
||||||
const isMobileView = ref(smAndDown.value);
|
|
||||||
|
|
||||||
watch(smAndDown, (newVal) => {
|
|
||||||
isMobileView.value = newVal;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gestion du slider (scroll sur petit écran)
|
|
||||||
const isDown = ref(false);
|
|
||||||
const startX = ref(0);
|
|
||||||
const scrollLeft = ref(0);
|
|
||||||
|
|
||||||
const mouseDown = (e) => {
|
|
||||||
const slider = document.querySelector('.custom-scroll');
|
|
||||||
isDown.value = true;
|
|
||||||
slider.classList.add('active');
|
|
||||||
startX.value = e.pageX - slider.offsetLeft;
|
|
||||||
scrollLeft.value = slider.scrollLeft;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseLeave = () => {
|
|
||||||
isDown.value = false;
|
|
||||||
const slider = document.querySelector('.custom-scroll');
|
|
||||||
slider.classList.remove('active');
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseUp = () => {
|
|
||||||
isDown.value = false;
|
|
||||||
const slider = document.querySelector('.custom-scroll');
|
|
||||||
slider.classList.remove('active');
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseMove = (e) => {
|
|
||||||
if (!isDown.value) return;
|
|
||||||
e.preventDefault();
|
|
||||||
const slider = document.querySelector('.custom-scroll');
|
|
||||||
const x = e.pageX - slider.offsetLeft;
|
|
||||||
const walk = (x - startX.value) * 3; // scroll-fast
|
|
||||||
slider.scrollLeft = scrollLeft.value - walk;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollLeftFunc = () => {
|
|
||||||
const container = document.querySelector('.custom-scroll');
|
|
||||||
container.scrollBy({ left: -100, behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollRightFunc = () => {
|
|
||||||
const container = document.querySelector('.custom-scroll');
|
|
||||||
container.scrollBy({ left: 100, behavior: 'smooth' });
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.custom-scroll {
|
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scroll::-webkit-scrollbar {
|
|
||||||
display: none; /* Safari and Chrome */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import {ref} from 'vue';
|
||||||
import { useClient } from '@/plugins/api.js';
|
import {useClient} from '@/plugins/api.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
creator: {
|
creator: {
|
||||||
@@ -14,7 +14,7 @@ const title = ref(props.creator.title);
|
|||||||
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
const save = async () => {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
await client.post(
|
await client.post(
|
||||||
`/api/creators/${props.creator.id}/title`,
|
`/api/creators/${props.creator.id}/title`,
|
||||||
@@ -28,7 +28,7 @@ const save = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving title:', error);
|
console.error('Error saving title:', error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
emits('closeRequested');
|
emits('closeRequested');
|
||||||
@@ -56,6 +56,7 @@ const cancel = () => {
|
|||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-y-4 > * + * {
|
.space-y-4 > * + * {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import XIcon from '@/assets/icons/x.svg';
|
|
||||||
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
|
|
||||||
import ChangeStripeID from '@/views/profile/creators/ChangeStripeID.vue';
|
|
||||||
import ChangeTitle from '@/views/profile/creators/ChangeTitle.vue';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import BannerPicker from './BannerPicker.vue';
|
|
||||||
import ColorsPicker from './ColorsPicker.vue';
|
|
||||||
import LogoPicker from './LogoPicker.vue';
|
|
||||||
import Socials from './Socials.vue';
|
|
||||||
|
|
||||||
const creatorProfileStore = useCreatorProfileStore();
|
|
||||||
console.log(creatorProfileStore.creator);
|
|
||||||
const imageBanner = computed(
|
|
||||||
() =>
|
|
||||||
creatorProfileStore.creator.images.banner ||
|
|
||||||
'/images/placeholders/banner.png'
|
|
||||||
);
|
|
||||||
const imageLogo = computed(
|
|
||||||
() =>
|
|
||||||
creatorProfileStore.creator.images.logo || '/images/placeholders/logo.png'
|
|
||||||
);
|
|
||||||
|
|
||||||
const dialog = ref(false);
|
|
||||||
const currentComponent = ref('');
|
|
||||||
|
|
||||||
const componentsMap = {
|
|
||||||
BannerPicker,
|
|
||||||
LogoPicker,
|
|
||||||
Socials,
|
|
||||||
ColorsPicker,
|
|
||||||
ChangeTitle,
|
|
||||||
ChangeStripeID,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function requestCancel() {
|
|
||||||
currentComponent.value = null;
|
|
||||||
dialog.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDialog = (component) => {
|
|
||||||
currentComponent.value = componentsMap[component];
|
|
||||||
dialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
currentComponent.value = null;
|
|
||||||
dialog.value = false;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-dialog v-model="dialog" max-width="800px">
|
|
||||||
<v-card
|
|
||||||
:style="{ borderRadius: '25px', border: '3px solid rgb(159, 76, 173)' }"
|
|
||||||
>
|
|
||||||
<v-card-text>
|
|
||||||
<component
|
|
||||||
:is="currentComponent"
|
|
||||||
:creator="creatorProfileStore.creator"
|
|
||||||
@closeRequested="closeDialog"
|
|
||||||
@requestAccept="requestAccept"
|
|
||||||
@requestCancel="requestCancel"
|
|
||||||
></component>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<!-- Lorsque l'utilisateur n'a pas de creator name-->
|
|
||||||
<div class="flex flex-col items-center w-full">
|
|
||||||
<h1 class="uppercase pb-5 text-2xl">
|
|
||||||
<v-icon class="mr-2">mdi-file-edit-outline</v-icon>
|
|
||||||
{{ $t('creatorinfopage.pageinformation') }}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div v-if="creatorProfileStore.hasCreator" class="w-full max-w-[800px]">
|
|
||||||
<div class="my-10 border rounded bg-white">
|
|
||||||
<div class="py-5 uppercase ml-4">
|
|
||||||
{{ $t('creatorinfopage.informations') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<button
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="flex-none pa-2 min-w-32 text-left">{{
|
|
||||||
$t('creatorinfopage.name')
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-auto text-left pr-6 capitalize">{{
|
|
||||||
creatorProfileStore.creator.name
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<button
|
|
||||||
@click="openDialog('ChangeTitle')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="flex-none pa-2 min-w-32 text-left">{{
|
|
||||||
$t('creatorinfopage.title')
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-auto text-left pr-6 capitalize">{{
|
|
||||||
creatorProfileStore.creator.title
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<button
|
|
||||||
@click="openDialog('ChangeStripeID')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full rounded-b"
|
|
||||||
>
|
|
||||||
<span class="flex-none pa-2 min-w-32 text-left"
|
|
||||||
>Stripe Account ID</span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.stripeId
|
|
||||||
}}</span>
|
|
||||||
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border rounded bg-white">
|
|
||||||
<div class="py-5 uppercase ml-4">
|
|
||||||
{{ $t('creatorinfopage.banner&profile') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col w-full gap-4">
|
|
||||||
<button
|
|
||||||
@click="openDialog('ColorsPicker')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6 capitalize">
|
|
||||||
Choisissez votre palette de couleurs.
|
|
||||||
</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button>
|
|
||||||
<img
|
|
||||||
@click="openDialog('BannerPicker')"
|
|
||||||
:src="imageBanner"
|
|
||||||
class="w-full transition duration-200 ease-in-out transform hover:brightness-125"
|
|
||||||
alt="Tutorial Banner"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="flex justify-center my-5">
|
|
||||||
<img
|
|
||||||
@click="openDialog('LogoPicker')"
|
|
||||||
class="custom-border hover:brightness-125 active:bg-gray-600 shadow flex items-center transition duration-200 ease-in-out w-48 h-48 rounded-full"
|
|
||||||
:src="imageLogo"
|
|
||||||
alt="Profile Image"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-10 border rounded bg-white">
|
|
||||||
<div class="py-5 uppercase ml-4">
|
|
||||||
{{ $t('creatorinfopage.socialnetwork') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full ">
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="pa-2 min-w-32 text-left"
|
|
||||||
><v-icon>mdi-facebook</v-icon></span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.facebookUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="flex-none pa-2 min-w-32 text-left">
|
|
||||||
<v-icon>mdi-instagram</v-icon></span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.instagramUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="flex-none pa-2 w-9 h-9 text-left ml-0.5">
|
|
||||||
<XIcon></XIcon>
|
|
||||||
</span>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.xUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="pa-2 min-w-32 text-left"
|
|
||||||
><v-icon>mdi-linkedin</v-icon></span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.linkedInUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="flex-none pa-2 min-w-32 text-left">
|
|
||||||
<XIcon class="w-5 h-5"></XIcon>
|
|
||||||
</span>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.tikTokUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="pa-2 min-w-32 text-left"
|
|
||||||
><v-icon>mdi-youtube</v-icon></span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.youtubeUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full"
|
|
||||||
>
|
|
||||||
<span class="pa-2 min-w-32 text-left"
|
|
||||||
><v-icon>mdi-reddit</v-icon></span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.redditUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="openDialog('Socials')"
|
|
||||||
class="HoverBtn active:bg-gray-300 py-2 px-4 border-gray-400 shadow flex items-center transition duration-200 ease-in-out w-full rounded-b"
|
|
||||||
>
|
|
||||||
<span class="pa-2 min-w-32 text-left"
|
|
||||||
><v-icon>mdi-web</v-icon></span
|
|
||||||
>
|
|
||||||
<span class="flex-auto text-left pr-6">{{
|
|
||||||
creatorProfileStore.creator.socials.websiteUrl
|
|
||||||
}}</span>
|
|
||||||
<span class="flex-none">
|
|
||||||
<v-icon>mdi-chevron-right</v-icon>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.HoverBtn:hover {
|
|
||||||
@apply bg-[#A6147D] text-white;
|
|
||||||
@apply hover:opacity-90;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-border {
|
|
||||||
border: 3px solid;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user