Added test logo file. Started adding community posts and comments

This commit is contained in:
Baumgartner Marcel 2023-10-27 15:10:44 +02:00
parent a29dc8257e
commit d98e8ef0f8
18 changed files with 1254 additions and 4 deletions

View file

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Community;
using Moonlight.App.Database.Entities.Store; using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Database.Entities.Tickets; using Moonlight.App.Database.Entities.Tickets;
using Moonlight.App.Services; using Moonlight.App.Services;
@ -26,6 +27,11 @@ public class DataContext : DbContext
public DbSet<Coupon> Coupons { get; set; } public DbSet<Coupon> Coupons { get; set; }
public DbSet<CouponUse> CouponUses { get; set; } public DbSet<CouponUse> CouponUses { get; set; }
// Posts
public DbSet<Post> Posts { get; set; }
public DbSet<PostComment> PostComments { get; set; }
public DbSet<PostLike> PostLikes { get; set; }
public DataContext(ConfigService configService) public DataContext(ConfigService configService)
{ {
ConfigService = configService; ConfigService = configService;

View file

@ -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<PostComment> Comments { get; set; } = new();
public List<PostLike> Likes { get; set; } = new();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Database.Enums;
public enum PostType
{
Project = 0,
Announcement = 1,
Event = 2
}

View file

@ -0,0 +1,539 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuthorId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.ToTable("Posts");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuthorId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int?>("PostId")
.HasColumnType("INTEGER");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int?>("PostId")
.HasColumnType("INTEGER");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Percent")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Coupons");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CouponId")
.HasColumnType("INTEGER");
b.Property<int?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Value")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("GiftCodes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GiftCodeId")
.HasColumnType("INTEGER");
b.Property<int?>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<string>("ConfigJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Duration")
.HasColumnType("INTEGER");
b.Property<int>("MaxPerUser")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Stock")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.ToTable("Products");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConfigJsonOverride")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<DateTime>("RenewAt")
.HasColumnType("TEXT");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Transaction");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Flags")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Permissions")
.HasColumnType("INTEGER");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("TEXT");
b.Property<string>("TotpKey")
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View file

@ -0,0 +1,131 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddPostsModels : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Posts",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Title = table.Column<string>(type: "TEXT", nullable: false),
Content = table.Column<string>(type: "TEXT", nullable: false),
AuthorId = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Content = table.Column<string>(type: "TEXT", nullable: false),
AuthorId = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
PostId = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
PostId = table.Column<int>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PostComments");
migrationBuilder.DropTable(
name: "PostLikes");
migrationBuilder.DropTable(
name: "Posts");
}
}
}

View file

@ -17,6 +17,94 @@ namespace Moonlight.App.Database.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuthorId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.ToTable("Posts");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuthorId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int?>("PostId")
.HasColumnType("INTEGER");
b.Property<DateTime>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int?>("PostId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PostId");
b.HasIndex("UserId");
b.ToTable("PostLikes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -299,6 +387,47 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("Users"); 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 => modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{ {
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
@ -381,6 +510,13 @@ namespace Moonlight.App.Database.Migrations
.HasForeignKey("UserId"); .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 => modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{ {
b.Navigation("Shares"); b.Navigation("Shares");

View file

@ -1,4 +1,5 @@
using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Community;
using Moonlight.App.Database.Entities.Store; using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Event.Args; using Moonlight.App.Event.Args;
@ -12,4 +13,10 @@ public class Events
public static EventHandler<MailVerificationEventArgs> OnUserMailVerify; public static EventHandler<MailVerificationEventArgs> OnUserMailVerify;
public static EventHandler<Service> OnServiceOrdered; public static EventHandler<Service> OnServiceOrdered;
public static EventHandler<TransactionCreatedEventArgs> OnTransactionCreated; public static EventHandler<TransactionCreatedEventArgs> OnTransactionCreated;
public static EventHandler<Post> OnPostCreated;
public static EventHandler<Post> OnPostUpdated;
public static EventHandler<Post> OnPostDeleted;
public static EventHandler<Post> OnPostLiked;
public static EventHandler<PostComment> OnPostCommentCreated;
public static EventHandler<PostComment> OnPostCommentDeleted;
} }

View file

@ -226,6 +226,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 // This will replace every placeholder with the respective value if specified in the model
// For example: // For example:
// A instance of the user model has been passed in the 'models' parameter of the function. // A instance of the user model has been passed in the 'models' parameter of the function.

View file

@ -9,6 +9,7 @@ public enum Permission
AdminSessions = 1002, AdminSessions = 1002,
AdminUsersEdit = 1003, AdminUsersEdit = 1003,
AdminTickets = 1004, AdminTickets = 1004,
AdminCommunity = 1030,
AdminStore = 1900, AdminStore = 1900,
AdminViewExceptions = 1999, AdminViewExceptions = 1999,
AdminRoot = 2000 AdminRoot = 2000

View file

@ -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<Post> PostRepository;
private readonly Repository<PostLike> PostLikeRepository;
private readonly Repository<PostComment> PostCommentRepository;
public PostService(Repository<Post> postRepository, Repository<PostLike> postLikeRepository, Repository<PostComment> postCommentRepository)
{
PostRepository = postRepository;
PostLikeRepository = postLikeRepository;
PostCommentRepository = postCommentRepository;
}
// Posts
public async Task<Post> 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<PostComment> 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);
}
}
}

View file

@ -19,7 +19,6 @@
<Folder Include="App\Http\Requests\" /> <Folder Include="App\Http\Requests\" />
<Folder Include="App\Http\Resources\" /> <Folder Include="App\Http\Resources\" />
<Folder Include="storage\logs\" /> <Folder Include="storage\logs\" />
<Folder Include="wwwroot\img\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -8,6 +8,7 @@ using Moonlight.App.Helpers.LogMigrator;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Background; using Moonlight.App.Services.Background;
using Moonlight.App.Services.Community;
using Moonlight.App.Services.Interop; using Moonlight.App.Services.Interop;
using Moonlight.App.Services.ServiceManage; using Moonlight.App.Services.ServiceManage;
using Moonlight.App.Services.Store; using Moonlight.App.Services.Store;
@ -58,6 +59,9 @@ builder.Services.AddScoped<StoreGiftService>();
builder.Services.AddSingleton<StorePaymentService>(); builder.Services.AddSingleton<StorePaymentService>();
builder.Services.AddScoped<TransactionService>(); builder.Services.AddScoped<TransactionService>();
// Services / Community
builder.Services.AddScoped<PostService>();
// Services / Users // Services / Users
builder.Services.AddScoped<UserService>(); builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<UserAuthService>(); builder.Services.AddScoped<UserAuthService>();

View file

@ -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<Post> PostRepository
@inject IdentityService IdentityService
@inject PostService PostService
<div class="card card-flush">
<div class="card-header pt-9">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-5">
<img src="/api/bucket/avatars/@(Post.Author.Avatar)" class="" alt="">
</div>
<div class="flex-grow-1">
<a href="#" class="text-gray-800 text-hover-primary fs-4 fw-bold">@(Post.Author.Username)</a>
<span class="text-gray-500 fw-semibold d-block">@(Formatter.FormatAgoFromDateTime(Post.CreatedAt))</span>
</div>
</div>
<div class="card-toolbar">
</div>
</div>
<div class="card-body">
<div class="fs-6 fw-normal text-gray-700">
@{
var sanitizer = new HtmlSanitizer();
var content = sanitizer.Sanitize(Post.Content);
@((MarkupString)content)
}
</div>
</div>
<div class="card-footer pt-0">
<div class="mb-6">
<div class="separator separator-solid"></div>
<ul class="nav py-3">
<li class="nav-item">
<a @onclick="ToggleComments" @onclick:preventDefault href="#" class="nav-link btn btn-sm btn-color-gray-600 btn-active-color-primary fw-bold px-4 me-1 @(ShowComments ? "active" : "")">
<i class="bx bx-message fs-2 me-1"></i>
@(CommentsCount) Comment(s)
</a>
</li>
<li class="nav-item">
<a @onclick="ToggleLike" @onclick:preventDefault href="#" class="nav-link btn btn-sm btn-color-gray-600 btn-active-color-danger fw-bold px-4 me-1 @(HasLiked ? "active" : "")">
<i class="bx bx-heart fs-2 me-1"></i>
@(LikesCount) Like(s)
</a>
</li>
</ul>
<div class="separator separator-solid mb-1"></div>
@if (ShowComments)
{
<LazyLoader Load="LoadComments">
@if (Comments.Any())
{
foreach (var comment in Comments)
{
<div class="d-flex pt-6">
<div class="symbol symbol-45px me-5">
<img src="/api/bucket/avatars/@(comment.Author.Avatar)" alt="">
</div>
<div class="d-flex flex-column flex-row-fluid">
<div class="d-flex align-items-center flex-wrap mb-0">
<a href="#" class="text-gray-800 text-hover-primary fw-bold me-6">@(comment.Author.Username)</a>
<span class="text-gray-500 fw-semibold fs-7 me-5">@(Formatter.FormatAgoFromDateTime(comment.CreatedAt))</span>
</div>
<span class="text-gray-800 fs-6 fw-normal pt-1">
@(Formatter.FormatLineBreaks(comment.Content))
</span>
</div>
</div>
}
}
else
{
<div class="d-flex pt-6 justify-content-center">
<span class="fs-5">No comments... yet</span>
</div>
}
<div class="separator separator-solid mt-6"></div>
</LazyLoader>
}
</div>
<div class="d-flex align-items-center">
<div class="symbol symbol-35px me-3">
<img src="/api/bucket/avatars/@(IdentityService.CurrentUser.Avatar)" alt="">
</div>
<div class="position-relative w-100">
<div class="input-group">
<textarea @bind="Comment" type="text" class="form-control form-control-solid border ps-5" placeholder="Write your comment.."></textarea>
<WButton OnClick="CreateComment" Text="Comment" CssClasses="btn btn-primary" />
</div>
</div>
</div>
</div>
</div>
@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<PostComment>();
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<PostComment>(); // Clear unused data
}
private async Task ToggleLike()
{
await PostService.ToggleLike(Post, IdentityService.CurrentUser);
await UpdateCounts();
}
}

View file

@ -6,9 +6,8 @@
<div class="app-sidebar flex-column @(Layout.ShowMobileSidebar ? "drawer drawer-start drawer-on" : "")"> <div class="app-sidebar flex-column @(Layout.ShowMobileSidebar ? "drawer drawer-start drawer-on" : "")">
<div class="app-sidebar-header d-flex flex-stack d-none d-lg-flex pt-8 pb-2"> <div class="app-sidebar-header d-flex flex-stack d-none d-lg-flex pt-8 pb-2">
<a href="/metronic8/demo38/../demo38/index.html" class="app-sidebar-logo"> <a href="/" class="app-sidebar-logo">
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/demo38.svg" class="h-25px d-none d-sm-inline app-sidebar-logo-default theme-light-show"> <img alt="Logo" src="/img/logo.png" class="h-50px d-none d-sm-inline app-sidebar-logo-default">
<img alt="Logo" src="/metronic8/demo38/assets/media/logos/demo38-dark.svg" class="h-20px h-lg-25px theme-dark-show">
</a> </a>
</div> </div>
<div class="app-sidebar-navs flex-column-fluid py-6" id="kt_app_sidebar_navs"> <div class="app-sidebar-navs flex-column-fluid py-6" id="kt_app_sidebar_navs">

View file

@ -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<Post> PostRepository
<div class="row">
<LazyLoader Load="Load">
@foreach (var post in Posts)
{
<PostView Post="post" />
<div class="mb-10"></div>
}
</LazyLoader>
</div>
@code
{
private Post[] Posts;
private Task Load(LazyLoader _)
{
Posts = PostRepository
.Get()
.Include(x => x.Author)
.ToArray();
return Task.CompletedTask;
}
}

BIN
Moonlight/wwwroot/img/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB