diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index 34a9d61..1789594 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; using Moonlight.App.Database.Entities.Tickets; using Moonlight.App.Services; @@ -25,6 +26,11 @@ public class DataContext : DbContext public DbSet Coupons { get; set; } public DbSet CouponUses { get; set; } + + // Posts + public DbSet Posts { get; set; } + public DbSet PostComments { get; set; } + public DbSet PostLikes { get; set; } public DataContext(ConfigService configService) { diff --git a/Moonlight/App/Database/Entities/Community/Post.cs b/Moonlight/App/Database/Entities/Community/Post.cs new file mode 100644 index 0000000..2d4af22 --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/Post.cs @@ -0,0 +1,16 @@ +using Moonlight.App.Database.Enums; + +namespace Moonlight.App.Database.Entities.Community; + +public class Post +{ + public int Id { get; set; } + public string Title { get; set; } = ""; + public string Content { get; set; } = ""; + public User Author { get; set; } + public PostType Type { get; set; } + public List Comments { get; set; } = new(); + public List Likes { get; set; } = new(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/Community/PostComment.cs b/Moonlight/App/Database/Entities/Community/PostComment.cs new file mode 100644 index 0000000..633bf41 --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/PostComment.cs @@ -0,0 +1,10 @@ +namespace Moonlight.App.Database.Entities.Community; + +public class PostComment +{ + public int Id { get; set; } + public string Content { get; set; } = ""; + public User Author { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/Community/PostLike.cs b/Moonlight/App/Database/Entities/Community/PostLike.cs new file mode 100644 index 0000000..0d0085d --- /dev/null +++ b/Moonlight/App/Database/Entities/Community/PostLike.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Database.Entities.Community; + +public class PostLike +{ + public int Id { get; set; } + public User User { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Enums/PostType.cs b/Moonlight/App/Database/Enums/PostType.cs new file mode 100644 index 0000000..4555a3b --- /dev/null +++ b/Moonlight/App/Database/Enums/PostType.cs @@ -0,0 +1,8 @@ +namespace Moonlight.App.Database.Enums; + +public enum PostType +{ + Project = 0, + Announcement = 1, + Event = 2 +} \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.Designer.cs b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.Designer.cs new file mode 100644 index 0000000..b1a2f6e --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.Designer.cs @@ -0,0 +1,539 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Moonlight.App.Database; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20231027105412_AddPostsModels")] + partial class AddPostsModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.cs b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.cs new file mode 100644 index 0000000..7910ab2 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231027105412_AddPostsModels.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddPostsModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Posts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Title = table.Column(type: "TEXT", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + AuthorId = table.Column(type: "INTEGER", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Posts", x => x.Id); + table.ForeignKey( + name: "FK_Posts_Users_AuthorId", + column: x => x.AuthorId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PostComments", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Content = table.Column(type: "TEXT", nullable: false), + AuthorId = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + PostId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PostComments", x => x.Id); + table.ForeignKey( + name: "FK_PostComments_Posts_PostId", + column: x => x.PostId, + principalTable: "Posts", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PostComments_Users_AuthorId", + column: x => x.AuthorId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PostLikes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + PostId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PostLikes", x => x.Id); + table.ForeignKey( + name: "FK_PostLikes_Posts_PostId", + column: x => x.PostId, + principalTable: "Posts", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PostLikes_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PostComments_AuthorId", + table: "PostComments", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_PostComments_PostId", + table: "PostComments", + column: "PostId"); + + migrationBuilder.CreateIndex( + name: "IX_PostLikes_PostId", + table: "PostLikes", + column: "PostId"); + + migrationBuilder.CreateIndex( + name: "IX_PostLikes_UserId", + table: "PostLikes", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Posts_AuthorId", + table: "Posts", + column: "AuthorId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PostComments"); + + migrationBuilder.DropTable( + name: "PostLikes"); + + migrationBuilder.DropTable( + name: "Posts"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 6cfcd7f..c759c09 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -17,6 +17,94 @@ namespace Moonlight.App.Database.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => { b.Property("Id") @@ -299,6 +387,47 @@ namespace Moonlight.App.Database.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => { b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") @@ -381,6 +510,13 @@ namespace Moonlight.App.Database.Migrations .HasForeignKey("UserId"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => { b.Navigation("Shares"); diff --git a/Moonlight/App/Event/Events.cs b/Moonlight/App/Event/Events.cs index 35d822e..d296728 100644 --- a/Moonlight/App/Event/Events.cs +++ b/Moonlight/App/Event/Events.cs @@ -1,4 +1,5 @@ using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; using Moonlight.App.Event.Args; @@ -12,4 +13,10 @@ public class Events public static EventHandler OnUserMailVerify; public static EventHandler OnServiceOrdered; public static EventHandler OnTransactionCreated; + public static EventHandler OnPostCreated; + public static EventHandler OnPostUpdated; + public static EventHandler OnPostDeleted; + public static EventHandler OnPostLiked; + public static EventHandler OnPostCommentCreated; + public static EventHandler OnPostCommentDeleted; } \ No newline at end of file diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index 2c4dc70..dadf049 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -225,6 +225,34 @@ public static class Formatter } }; } + + public static string FormatAgoFromDateTime(DateTime dt) + { + TimeSpan timeSince = DateTime.UtcNow.Subtract(dt); + + if (timeSince.TotalMilliseconds < 1) + return "just now"; + + if (timeSince.TotalMinutes < 1) + return "less than a minute ago"; + + if (timeSince.TotalMinutes < 2) + return "1 minute ago"; + + if (timeSince.TotalMinutes < 60) + return Math.Round(timeSince.TotalMinutes) + " minutes ago"; + + if (timeSince.TotalHours < 2) + return "1 hour ago"; + + if (timeSince.TotalHours < 24) + return Math.Round(timeSince.TotalHours) + " hours ago"; + + if (timeSince.TotalDays < 2) + return "1 day ago"; + + return Math.Round(timeSince.TotalDays) + " days ago"; + } // This will replace every placeholder with the respective value if specified in the model // For example: diff --git a/Moonlight/App/Models/Enums/Permission.cs b/Moonlight/App/Models/Enums/Permission.cs index 3bb7e62..677bf9d 100644 --- a/Moonlight/App/Models/Enums/Permission.cs +++ b/Moonlight/App/Models/Enums/Permission.cs @@ -9,6 +9,7 @@ public enum Permission AdminSessions = 1002, AdminUsersEdit = 1003, AdminTickets = 1004, + AdminCommunity = 1030, AdminStore = 1900, AdminViewExceptions = 1999, AdminRoot = 2000 diff --git a/Moonlight/App/Services/Community/PostService.cs b/Moonlight/App/Services/Community/PostService.cs new file mode 100644 index 0000000..aed5986 --- /dev/null +++ b/Moonlight/App/Services/Community/PostService.cs @@ -0,0 +1,135 @@ +using System.Text.RegularExpressions; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Database.Entities.Community; +using Moonlight.App.Database.Enums; +using Moonlight.App.Event; +using Moonlight.App.Exceptions; +using Moonlight.App.Extensions; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Community; + +public class PostService +{ + private readonly Repository PostRepository; + private readonly Repository PostLikeRepository; + private readonly Repository PostCommentRepository; + + public PostService(Repository postRepository, Repository postLikeRepository, Repository postCommentRepository) + { + PostRepository = postRepository; + PostLikeRepository = postLikeRepository; + PostCommentRepository = postCommentRepository; + } + + // Posts + public async Task Create(User user, string title, string content, PostType type) + { + var post = new Post() + { + Author = user, + Title = title, + Content = content, + Type = type + }; + + var finishedPost = PostRepository.Add(post); + + await Events.OnPostCreated.InvokeAsync(finishedPost); + + return finishedPost; + } + + public async Task Update(Post post, string title, string content) + { + post.Title = title; + post.Content = content; + + PostRepository.Update(post); + + await Events.OnPostUpdated.InvokeAsync(post); + } + + public async Task Delete(Post post) + { + PostRepository.Delete(post); + await Events.OnPostDeleted.InvokeAsync(post); + } + + // Comments + public async Task CreateComment(Post post, User user, string content) + { + // As the comment feature has no edit form or model to validate we do the validation here + if (string.IsNullOrEmpty(content)) + throw new DisplayException("Comment content cannot be empty"); + + if (content.Length > 1024) + throw new DisplayException("Comment content cannot be longer than 1024 characters"); + + if (!Regex.IsMatch(content, "^[a-zA-Z0-9äöüßÄÖÜẞ,.;_\\n\\t-]+$")) + throw new DisplayException("Illegal characters in comment content"); + + //TODO: Swear word filter + + var comment = new PostComment() + { + Author = user, + Content = content + }; + + post.Comments.Add(comment); + PostRepository.Update(post); + + await Events.OnPostCommentCreated.InvokeAsync(comment); + + return comment; + } + + public async Task DeleteComment(Post post, PostComment comment) + { + var postWithComments = PostRepository + .Get() + .Include(x => x.Comments) + .First(x => x.Id == post.Id); + + var commentToRemove = postWithComments.Comments.First(x => x.Id == comment.Id); + postWithComments.Comments.Remove(commentToRemove); + + PostRepository.Update(postWithComments); + PostCommentRepository.Delete(commentToRemove); + + await Events.OnPostCommentCreated.InvokeAsync(commentToRemove); + } + + // Other + public async Task ToggleLike(Post post, User user) + { + var postWithLikes = PostRepository + .Get() + .Include(x => x.Likes) + .ThenInclude(x => x.User) + .First(x => x.Id == post.Id); + + var userLike = postWithLikes.Likes.FirstOrDefault(x => x.User.Id == user.Id); + + if (userLike != null) // Check if person already liked + { + postWithLikes.Likes.Remove(userLike); + + PostRepository.Update(postWithLikes); + PostLikeRepository.Delete(userLike); + } + else + { + postWithLikes.Likes.Add(new() + { + User = user + }); + + PostRepository.Update(postWithLikes); + + await Events.OnPostLiked.InvokeAsync(postWithLikes); + } + } +} \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 79add5b..e6aec37 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -19,7 +19,6 @@ - diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index fe9ebe1..dca2ceb 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -8,6 +8,7 @@ using Moonlight.App.Helpers.LogMigrator; using Moonlight.App.Repositories; using Moonlight.App.Services; using Moonlight.App.Services.Background; +using Moonlight.App.Services.Community; using Moonlight.App.Services.Interop; using Moonlight.App.Services.ServiceManage; using Moonlight.App.Services.Store; @@ -58,6 +59,9 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); +// Services / Community +builder.Services.AddScoped(); + // Services / Users builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Components/Community/PostView.razor b/Moonlight/Shared/Components/Community/PostView.razor new file mode 100644 index 0000000..b428f2b --- /dev/null +++ b/Moonlight/Shared/Components/Community/PostView.razor @@ -0,0 +1,190 @@ +@using Moonlight.App.Database.Entities.Community +@using Ganss.Xss +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Moonlight.App.Services.Community + +@inject Repository PostRepository +@inject IdentityService IdentityService +@inject PostService PostService + +
+
+
+
+ +
+
+ @(Post.Author.Username) + @(Formatter.FormatAgoFromDateTime(Post.CreatedAt)) +
+
+
+ +
+
+
+
+ @{ + var sanitizer = new HtmlSanitizer(); + var content = sanitizer.Sanitize(Post.Content); + @((MarkupString)content) + } +
+
+ +
+ +@code +{ + [Parameter] + public Post Post { get; set; } + + private int CommentsCount = -1; + private int LikesCount = -1; + private bool HasLiked = false; + + private bool ShowComments = false; + private PostComment[] Comments = Array.Empty(); + private string Comment = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await UpdateCounts(); + } + } + + private Task LoadComments(LazyLoader _) + { + Comments = PostRepository + .Get() + .Include(x => x.Comments) + .ThenInclude(x => x.Author) + .First(x => x.Id == Post.Id) + .Comments + .ToArray(); + + return Task.CompletedTask; + } + + private async Task UpdateCounts() + { + CommentsCount = PostRepository + .Get() + .Where(x => x.Id == Post.Id) + .SelectMany(x => x.Comments) + .Count(); + + LikesCount = PostRepository + .Get() + .Where(x => x.Id == Post.Id) + .SelectMany(x => x.Likes) + .Count(); + + HasLiked = PostRepository + .Get() + .Where(x => x.Id == Post.Id) + .SelectMany(x => x.Likes) + .Any(x => x.User.Id == IdentityService.CurrentUser.Id); + + await InvokeAsync(StateHasChanged); + } + + private async Task CreateComment() + { + await PostService.CreateComment(Post, IdentityService.CurrentUser, Comment); + + Comment = ""; + ShowComments = true; + await InvokeAsync(StateHasChanged); + await UpdateCounts(); + } + + private async Task DeleteComment(PostComment comment) + { + await PostService.DeleteComment(Post, comment); + + await InvokeAsync(StateHasChanged); + await UpdateCounts(); + } + + private async Task ToggleComments() + { + ShowComments = !ShowComments; + await InvokeAsync(StateHasChanged); + + if (!ShowComments) + Comments = Array.Empty(); // Clear unused data + } + + private async Task ToggleLike() + { + await PostService.ToggleLike(Post, IdentityService.CurrentUser); + await UpdateCounts(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Partials/Sidebar.razor b/Moonlight/Shared/Components/Partials/Sidebar.razor index 2b4382c..004e62a 100644 --- a/Moonlight/Shared/Components/Partials/Sidebar.razor +++ b/Moonlight/Shared/Components/Partials/Sidebar.razor @@ -6,9 +6,8 @@
diff --git a/Moonlight/Shared/Views/Community/Index.razor b/Moonlight/Shared/Views/Community/Index.razor new file mode 100644 index 0000000..a577f0d --- /dev/null +++ b/Moonlight/Shared/Views/Community/Index.razor @@ -0,0 +1,33 @@ +@page "/community" + +@using Moonlight.App.Repositories +@using Moonlight.App.Database.Entities.Community +@using Microsoft.EntityFrameworkCore +@using Moonlight.Shared.Components.Community + +@inject Repository PostRepository + +
+ + @foreach (var post in Posts) + { + +
+ } +
+
+ +@code +{ + private Post[] Posts; + + private Task Load(LazyLoader _) + { + Posts = PostRepository + .Get() + .Include(x => x.Author) + .ToArray(); + + return Task.CompletedTask; + } +} diff --git a/Moonlight/wwwroot/img/logo.png b/Moonlight/wwwroot/img/logo.png new file mode 100644 index 0000000..77737b9 Binary files /dev/null and b/Moonlight/wwwroot/img/logo.png differ