Merge pull request #330 from Moonlight-Panel/AddCommunity

Added community tab
This commit is contained in:
Marcel Baumgartner 2023-10-29 11:15:05 +01:00 committed by GitHub
commit c3acb4898e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2638 additions and 12 deletions

View file

@ -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,12 @@ public class DataContext : DbContext
public DbSet<Coupon> Coupons { get; set; }
public DbSet<CouponUse> CouponUses { get; set; }
// Community
public DbSet<Post> Posts { get; set; }
public DbSet<PostComment> PostComments { get; set; }
public DbSet<PostLike> PostLikes { get; set; }
public DbSet<WordFilter> WordFilters { get; set; }
public DataContext(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,7 @@
namespace Moonlight.App.Database.Entities.Community;
public class WordFilter
{
public int Id { get; set; }
public string Filter { get; set; } = "";
}

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

@ -0,0 +1,554 @@
// <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("20231028214520_AddedWordFilter")]
partial class AddedWordFilter
{
/// <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.Community.WordFilter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Filter")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("WordFilters");
});
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,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedWordFilter : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "WordFilters",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Filter = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WordFilters", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WordFilters");
}
}
}

View file

@ -17,6 +17,109 @@ 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<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.Community.WordFilter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Filter")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("WordFilters");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
{
b.Property<int>("Id")
@ -299,6 +402,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 +525,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");

View file

@ -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<MailVerificationEventArgs> OnUserMailVerify;
public static EventHandler<Service> OnServiceOrdered;
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

@ -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:

View file

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

View file

@ -0,0 +1,12 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Admin.Community;
public class AddWordFilter
{
[Required(ErrorMessage = "You need to specify a filter")]
[Description(
"This filters all posts and comments created using this regex. If any match is found it will block the action")]
public string Filter { get; set; } = "";
}

View file

@ -0,0 +1,12 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Admin.Community;
public class EditWordFilter
{
[Required(ErrorMessage = "You need to specify a filter")]
[Description(
"This filters all posts and comments created using this regex. If any match is found it will block the action")]
public string Filter { get; set; } = "";
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Community;
public class AddPostForm
{
[Required(ErrorMessage = "You need to enter a title")]
[MaxLength(40, ErrorMessage = "The title can only be 40 characters long")]
[MinLength(8, ErrorMessage = "The title must at least have 8 characters")]
public string Title { get; set; } = "";
[Required(ErrorMessage = "You need to enter post content")]
[MaxLength(2048, ErrorMessage = "The post content can only be 2048 characters long")]
[MinLength(8, ErrorMessage = "The post content must at least have 8 characters")]
public string Content { get; set; } = "";
}

View file

@ -0,0 +1,200 @@
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;
private readonly Repository<WordFilter> WordFilterRepository;
public PostService(
Repository<Post> postRepository,
Repository<PostLike> postLikeRepository,
Repository<PostComment> postCommentRepository,
Repository<WordFilter> wordFilterRepository)
{
PostRepository = postRepository;
PostLikeRepository = postLikeRepository;
PostCommentRepository = postCommentRepository;
WordFilterRepository = wordFilterRepository;
}
// Posts
public async Task<Post> Create(User user, string title, string content, PostType type)
{
if(await CheckTextForBadWords(title))
throw new DisplayException("Bad word detected. Please follow the community rules");
if(await CheckTextForBadWords(content))
throw new DisplayException("Bad word detected. Please follow the community rules");
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)
{
if(await CheckTextForBadWords(title))
throw new DisplayException("Bad word detected. Please follow the community rules");
if(await CheckTextForBadWords(content))
throw new DisplayException("Bad word detected. Please follow the community rules");
post.Title = title;
post.Content = content;
post.UpdatedAt = DateTime.UtcNow;
PostRepository.Update(post);
await Events.OnPostUpdated.InvokeAsync(post);
}
public async Task Delete(Post post)
{
var postWithData = PostRepository
.Get()
.Include(x => x.Comments)
.Include(x => x.Likes)
.First(x => x.Id == post.Id);
// Cache relational data to delete later on
var likes = postWithData.Likes.ToArray();
var comments = postWithData.Comments.ToArray();
// Clear relations
postWithData.Comments.Clear();
postWithData.Likes.Clear();
PostRepository.Update(postWithData);
// Delete relational data
foreach (var like in likes)
PostLikeRepository.Delete(like);
foreach (var comment in comments)
PostCommentRepository.Delete(comment);
// Now delete the post itself
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");
if(await CheckTextForBadWords(content))
throw new DisplayException("Bad word detected. Please follow the community rules");
//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);
}
}
// Utils
private Task<bool> CheckTextForBadWords(string input) // This method checks for bad words using the filters added by an admin
{
var filters = WordFilterRepository
.Get()
.Select(x => x.Filter)
.ToArray();
//TODO: Add timer for regex matching to create warnings
foreach (var filter in filters)
{
if (Regex.IsMatch(input, filter))
return Task.FromResult(true);
}
return Task.FromResult(false);
}
}

View file

@ -11,11 +11,11 @@ public class ModalService
JsRuntime = jsRuntime;
}
public async Task Show(string id)
public async Task Show(string id, bool focus = true) // Focus can be specified to fix issues with other components
{
try
{
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id);
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id, focus);
}
catch (Exception)
{

View file

@ -19,12 +19,12 @@
<Folder Include="App\Http\Requests\" />
<Folder Include="App\Http\Resources\" />
<Folder Include="storage\logs\" />
<Folder Include="wwwroot\img\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
<PackageReference Include="BlazorTable" Version="1.17.0" />
<PackageReference Include="HtmlSanitizer" Version="8.0.746" />
<PackageReference Include="JWT" Version="10.1.1" />
<PackageReference Include="MailKit" Version="4.2.0" />
<PackageReference Include="Mappy.Net" Version="1.0.2" />

View file

@ -37,6 +37,7 @@
<script src="/js/moonlight.js"></script>
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
<script src="/js/sweetalert2.js"></script>
<script src="/js/ckeditor.js"></script>
<script src="/_framework/blazor.server.js"></script>
</body>
</html>

View file

@ -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<StoreGiftService>();
builder.Services.AddSingleton<StorePaymentService>();
builder.Services.AddScoped<TransactionService>();
// Services / Community
builder.Services.AddScoped<PostService>();
// Services / Users
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<UserAuthService>();

View file

@ -0,0 +1,272 @@
@using Moonlight.App.Database.Entities.Community
@using Ganss.Xss
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Models.Enums
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Moonlight.App.Services.Community
@inject Repository<Post> PostRepository
@inject IdentityService IdentityService
@inject PostService PostService
@inject ToastService ToastService
<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">
<span class="text-gray-800 text-hover-primary fs-4 fw-bold">@(Post.Author.Username)</span>
<span class="text-gray-500 fw-semibold d-block">@(Formatter.FormatAgoFromDateTime(Post.CreatedAt))</span>
</div>
</div>
@if (Post.Author.Id == IdentityService.CurrentUser.Id || IdentityService.Permissions[Permission.AdminCommunity])
{
<div class="card-toolbar">
<a @onclick="DeletePost" @onclick:preventDefault href="#" class="text-danger fw-semibold d-block">Remove post</a>
</div>
}
</div>
<div class="card-body">
<div class="fs-6 fw-normal text-gray-700">
@if (IsEditing)
{
<TextEditor @bind-Value="EditContent" InitialContent="@Post.Content" />
}
else
{
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" : "")">
@if (HasLiked)
{
<i class="bx bxs-heart fs-2 me-1"></i>
}
else
{
<i class="bx bx-heart fs-2 me-1"></i>
}
@(LikesCount) Like(s)
</a>
</li>
@if (Post.Author.Id == IdentityService.CurrentUser.Id || IdentityService.Permissions[Permission.AdminCommunity])
{
<li class="nav-item">
<div class="nav-link pt-0">
<a @onclick="() => ToggleEdit()" @onclick:preventDefault href="#" class="btn btn-sm btn-color-gray-600 btn-active-color-warning fw-bold px-4 pe-0 @(IsEditing ? "active" : "")">
<i class="bx bx-edit fs-2"></i>
@(IsEditing ? "Save" : "Edit")
</a>
@if (IsEditing)
{
<a @onclick="() => ToggleEdit(true)" @onclick:preventDefault href="#" class="btn btn-sm btn-color-gray-600 btn-active-color-warning fw-bold px-4 ps-2 @(IsEditing ? "active" : "")">
<i class="bx bx-x fs-2"></i>
Cancel
</a>
}
</div>
</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>
@if (comment.Author.Id == IdentityService.CurrentUser.Id || IdentityService.Permissions[Permission.AdminCommunity])
{
<a @onclick="() => DeleteComment(comment)" @onclick:preventDefault href="#" class="text-danger fw-semibold fs-7">Remove comment</a>
}
</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.." style="height: 1vh"></textarea>
<WButton OnClick="CreateComment" Text="Comment" CssClasses="btn btn-primary"/>
</div>
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public Post Post { get; set; }
[Parameter]
public Func<Task>? OnUpdate { 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 = "";
private bool IsEditing = false;
private string EditTitle = "";
private string EditContent = "";
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
.OrderBy(x => x.CreatedAt)
.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 LoadComments(null!);
await InvokeAsync(StateHasChanged);
await UpdateCounts();
}
private async Task DeleteComment(PostComment comment)
{
await PostService.DeleteComment(Post, comment);
await LoadComments(null!);
await InvokeAsync(StateHasChanged);
await UpdateCounts();
await ToastService.Success("Successfully deleted comment");
}
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();
}
private async Task ToggleEdit(bool preventSaving = false)
{
IsEditing = !IsEditing;
if (IsEditing)
{
EditTitle = Post.Title;
EditContent = Post.Content;
}
else if (!preventSaving)
{
await PostService.Update(Post, EditTitle, EditContent);
await ToastService.Success("Successfully saved post");
}
await InvokeAsync(StateHasChanged);
}
private async Task DeletePost()
{
await PostService.Delete(Post);
await ToastService.Success("Successfully deleted post");
if (OnUpdate != null)
await OnUpdate.Invoke();
}
}

View file

@ -54,7 +54,7 @@
<SmartForm Model="CreateForm" OnValidSubmit="FinishCreate">
<div class="modal-body">
<div class="row">
<AutoForm Columns="6" Model="CreateForm"/>
<AutoForm Columns="@(CreateForm.GetType().GetProperties().Length > 1 ? 6 : 12)" Model="CreateForm"/>
</div>
</div>
<div class="modal-footer">
@ -72,7 +72,7 @@
<SmartForm Model="UpdateForm" OnValidSubmit="FinishUpdate">
<div class="modal-body">
<div class="row">
<AutoForm Columns="6" Model="UpdateForm"/>
<AutoForm Columns="@(UpdateForm.GetType().GetProperties().Length > 1 ? 6 : 12)" Model="UpdateForm"/>
</div>
</div>
<div class="modal-footer">

View file

@ -0,0 +1,175 @@
@inject IJSRuntime JsRuntime
@using Microsoft.AspNetCore.Components.Forms
@using Moonlight.App.Services
@using Ganss.Xss
@inherits InputBase<string>
@inject IdentityService IdentityService
<style>
:root {
/*
Fix from https://ckeditor.com/docs/ckeditor5/latest/installation/integrations/css.html
to make the editor work with bootstrap
*/
--ck-z-default: 100;
--ck-z-modal: calc( var(--ck-z-default) + 999 );
/* Overrides the border radius setting in the theme. */
--ck-border-radius: 4px;
/* Overrides the default font size in the theme. */
--ck-font-size-base: 14px;
/* Helper variables to avoid duplication in the colors. */
--ck-custom-background: #1e1e2d;
--ck-custom-foreground: hsl(255, 3%, 18%);
--ck-custom-border: hsl(300, 1%, 22%);
--ck-custom-white: hsl(0, 0%, 100%);
/* -- Overrides generic colors. ------------------------------------------------------------- */
--ck-color-base-foreground: var(--ck-custom-background);
--ck-color-focus-border: hsl(208, 90%, 62%);
--ck-color-text: hsl(0, 0%, 98%);
--ck-color-shadow-drop: hsla(0, 0%, 0%, 0.2);
--ck-color-shadow-inner: hsla(0, 0%, 0%, 0.1);
/* -- Overrides the default .ck-button class colors. ---------------------------------------- */
--ck-color-button-default-background: var(--ck-custom-background);
--ck-color-button-default-hover-background: hsl(270, 1%, 22%);
--ck-color-button-default-active-background: hsl(270, 2%, 20%);
--ck-color-button-default-active-shadow: hsl(270, 2%, 23%);
--ck-color-button-default-disabled-background: var(--ck-custom-background);
--ck-color-button-on-background: var(--ck-custom-foreground);
--ck-color-button-on-hover-background: hsl(255, 4%, 16%);
--ck-color-button-on-active-background: hsl(255, 4%, 14%);
--ck-color-button-on-active-shadow: hsl(240, 3%, 19%);
--ck-color-button-on-disabled-background: var(--ck-custom-foreground);
--ck-color-button-action-background: hsl(168, 76%, 42%);
--ck-color-button-action-hover-background: hsl(168, 76%, 38%);
--ck-color-button-action-active-background: hsl(168, 76%, 36%);
--ck-color-button-action-active-shadow: hsl(168, 75%, 34%);
--ck-color-button-action-disabled-background: hsl(168, 76%, 42%);
--ck-color-button-action-text: var(--ck-custom-white);
/* -- Overrides the default .ck-dropdown class colors. -------------------------------------- */
--ck-color-dropdown-panel-background: var(--ck-custom-background);
--ck-color-dropdown-panel-border: var(--ck-custom-foreground);
/* -- Overrides the default .ck-splitbutton class colors. ----------------------------------- */
--ck-color-split-button-hover-background: var(--ck-color-button-default-hover-background);
--ck-color-split-button-hover-border: var(--ck-custom-foreground);
/* -- Overrides the default .ck-input class colors. ----------------------------------------- */
--ck-color-input-background: var(--ck-custom-background);
--ck-color-input-border: hsl(257, 3%, 43%);
--ck-color-input-text: hsl(0, 0%, 98%);
--ck-color-input-disabled-background: hsl(255, 4%, 21%);
--ck-color-input-disabled-border: hsl(250, 3%, 38%);
--ck-color-input-disabled-text: hsl(0, 0%, 78%);
/* -- Overrides the default .ck-labeled-field-view class colors. ---------------------------- */
--ck-color-labeled-field-label-background: var(--ck-custom-background);
/* -- Overrides the default .ck-list class colors. ------------------------------------------ */
--ck-color-list-background: var(--ck-custom-background);
--ck-color-list-button-hover-background: var(--ck-custom-foreground);
--ck-color-list-button-on-background: hsl(208, 88%, 52%);
--ck-color-list-button-on-text: var(--ck-custom-white);
/* -- Overrides the default .ck-balloon-panel class colors. --------------------------------- */
--ck-color-panel-background: var(--ck-custom-background);
--ck-color-panel-border: var(--ck-custom-border);
/* -- Overrides the default .ck-toolbar class colors. --------------------------------------- */
--ck-color-toolbar-background: var(--ck-custom-background);
--ck-color-toolbar-border: var(--ck-custom-border);
/* -- Overrides the default .ck-tooltip class colors. --------------------------------------- */
--ck-color-tooltip-background: hsl(252, 7%, 14%);
--ck-color-tooltip-text: hsl(0, 0%, 93%);
/* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
/* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */
--ck-color-widget-blurred-border: hsl(0, 0%, 87%);
--ck-color-widget-hover-border: hsl(43, 100%, 68%);
--ck-color-widget-editable-focus-background: var(--ck-custom-white);
/* -- Overrides the default colors used by the ckeditor5-link package. ---------------------- */
--ck-color-link-default: hsl(190, 100%, 75%);
}
</style>
<div id="@Id" class="card card-body bg-black @(CssClasses)" @onfocusout="Callback" style="@(Styles)"></div>
@code
{
[Parameter]
public string InitialContent { get; set; }
[Parameter]
public string CssClasses { get; set; } = "";
[Parameter]
public string Styles { get; set; } = ""; // We added this parameter to allow custom heights to be set
private string Id;
private bool IsInitialized = false;
protected override void OnInitialized()
{
Id = "editor" + GetHashCode();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JsRuntime.InvokeVoidAsync("moonlight.textEditor.create", Id);
await JsRuntime.InvokeVoidAsync("moonlight.textEditor.set", Id, InitialContent);
CurrentValue = InitialContent;
IsInitialized = true;
}
}
private async Task Callback()
{
if(!IsInitialized)
return;
var html = await JsRuntime.InvokeAsync<string>("moonlight.textEditor.get", Id);
var sanitizer = new HtmlSanitizer();
var sanitized = sanitizer.Sanitize(html);
if(sanitized != html)
Logger.Warn($"XSS attempt by {IdentityService.CurrentUserNullable?.Username ?? "Guest"}: {html}", "security");
CurrentValue = sanitized;
}
protected override bool TryParseValueFromString(string? value, out string result, out string? validationErrorMessage)
{
result = value;
validationErrorMessage = "";
return true;
}
}

View file

@ -0,0 +1,67 @@
@using Moonlight.App.Models.Forms.Community
@using Moonlight.App.Services
@using Moonlight.App.Services.Community
@using Moonlight.App.Database.Enums
@inject PostService PostService
@inject IdentityService IdentityService
@inject ToastService ToastService
<SmartModal @ref="Modal" CssClasses="modal-fullscreen">
<div class="modal-header">
<h5 class="modal-title fs-3">Create a new post</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<SmartForm Model="Form" OnValidSubmit="Submit">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<div class="form-text fs-5 mb-2 mt-0">
This title is used for the preview of posts. It will not be shown in a regular post view
</div>
<input @bind="Form.Title" class="form-control form-control-solid-bg"/>
</div>
<div>
<TextEditor @bind-Value="Form.Content" InitialContent="A well written post content from you" Styles="height: 55vh"/>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</SmartForm>
</SmartModal>
@code
{
[Parameter]
public Func<Task>? OnUpdate { get; set; }
[Parameter]
public PostType PostType { get; set; }
private AddPostForm Form = new();
private SmartModal Modal;
public async Task Show()
{
Form = new();
await Modal.Show(false);
}
private async Task Submit()
{
await PostService.Create(
IdentityService.CurrentUser,
Form.Title,
Form.Content,
PostType
);
await Modal.Hide();
await ToastService.Success("Successfully created post");
if (OnUpdate != null)
await OnUpdate.Invoke();
}
}

View file

@ -0,0 +1,22 @@
<div class="card mb-5 mb-xl-10">
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/community">
<i class="bx bx-sm bx-group me-2"></i> Overview
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/community/filter">
<i class="bx bx-sm bx-filter-alt me-2"></i> Filter
</a>
</li>
</ul>
</div>
</div>
@code
{
[Parameter]
public int Index { get; set; }
}

View file

@ -0,0 +1,41 @@
<div class="card">
<div class="card-header border-0 pt-5">
<h3 class="card-title align-items-start flex-column">
<span class="card-label fw-bold text-gray-900">Post channels</span>
</h3>
</div>
<div class="card-body pt-5">
<div class="d-flex flex-stack">
<div class="symbol symbol-40px me-5">
<i class="bx bx-sm bx-envelope text-gray-500"></i>
</div>
<div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/community" class="text-gray-800 fs-5 fw-bold text-active-primary @(Index == 0 ? "active" : "")">Announcements</a>
</div>
</div>
<div class="separator separator-dashed my-4"></div>
<div class="d-flex flex-stack">
<div class="symbol symbol-40px me-5">
<i class="bx bx-sm bx-calendar-event text-gray-500"></i>
</div>
<div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/community/events" class="text-gray-800 fs-5 fw-bold text-active-primary @(Index == 1 ? "active" : "")">Events</a>
</div>
</div>
<div class="separator separator-dashed my-4"></div>
<div class="d-flex flex-stack">
<div class="symbol symbol-40px me-5">
<i class="bx bx-sm bxs-inbox text-gray-500"></i>
</div>
<div class="d-flex align-items-center flex-row-fluid flex-wrap">
<a href="/community/projects" class="text-gray-800 fs-5 fw-bold text-active-primary @(Index == 2 ? "active" : "")">Projects</a>
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public int Index { get; set; }
}

View file

@ -6,9 +6,8 @@
<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">
<a href="/metronic8/demo38/../demo38/index.html" 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="/metronic8/demo38/assets/media/logos/demo38-dark.svg" class="h-20px h-lg-25px theme-dark-show">
<a href="/" class="app-sidebar-logo">
<img alt="Logo" src="/img/logo.png" class="h-50px d-none d-sm-inline app-sidebar-logo-default">
</a>
</div>
<div class="app-sidebar-navs flex-column-fluid py-6" id="kt_app_sidebar_navs">
@ -95,6 +94,17 @@
</span>
</a>
</div>
<div class="menu-item">
<a class="menu-link " href="/admin/community">
<span class="menu-icon">
<i class="bx bx-sm bx-group"></i>
</span>
<span class="menu-title">
Community
</span>
</a>
</div>
}
</div>
</div>

View file

@ -27,12 +27,12 @@
Id = GetHashCode();
}
public async Task Show()
public async Task Show(bool focus = true) // Focus can be specified to fix issues with other components
{
ShouldShow = true;
await InvokeAsync(StateHasChanged);
await ModalService.Show("modal" + Id);
await ModalService.Show("modal" + Id, focus);
}
public async Task Hide()

View file

@ -0,0 +1,38 @@
@page "/admin/community/filter"
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Models.Enums
@using BlazorTable
@using Moonlight.App.Database.Entities.Community
@using Moonlight.App.Models.Forms.Admin.Community
@using Moonlight.App.Repositories
@attribute [RequirePermission(Permission.AdminCommunity)]
<AdminCommunityNavigation Index="1" />
<div class="card card-body border-primary fs-5 mt-5">
To protect from trollers and toxic people you can configure words using
regex expressions to block automatically to ensure no one can write bad things in the community tab.
</div>
<div class="mt-5">
<AutoCrud TItem="WordFilter"
TCreateForm="AddWordFilter"
TUpdateForm="EditWordFilter"
Title="Manage word filter"
Load="LoadData">
<Column TableItem="WordFilter" Field="@(x => x.Id)" Title="Id" Sortable="false" Filterable="true" />
<Column TableItem="WordFilter" Field="@(x => x.Filter)" Title="Filter" Sortable="false" Filterable="true" />
</AutoCrud>
</div>
@code
{
private WordFilter[] LoadData(Repository<WordFilter> repository)
{
return repository
.Get()
.ToArray();
}
}

View file

@ -0,0 +1,8 @@
@page "/admin/community"
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Models.Enums
@attribute [RequirePermission(Permission.AdminCommunity)]
<AdminCommunityNavigation Index="0" />

View file

@ -3,8 +3,12 @@
@using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Repositories
@using BlazorTable
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Models.Enums
@using Moonlight.App.Models.Forms.Admin.Store
@attribute [RequirePermission(Permission.AdminStore)]
@inject Repository<Coupon> CouponRepository
<AdminStoreNavigation Index="1"/>

View file

@ -4,6 +4,10 @@
@using Moonlight.App.Models.Forms.Admin.Store
@using Moonlight.App.Repositories
@using BlazorTable
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Models.Enums
@attribute [RequirePermission(Permission.AdminStore)]
@inject Repository<GiftCode> GiftCodeRepository

View file

@ -1,3 +1,8 @@
@page "/admin/store"
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Models.Enums
@attribute [RequirePermission(Permission.AdminStore)]
<AdminStoreNavigation Index="0"/>

View file

@ -0,0 +1,66 @@
@page "/community/events"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities.Community
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Enums
@using Moonlight.App.Models.Enums
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Community
@using Moonlight.Shared.Components.Modals.Community
@inject IdentityService IdentityService
@inject Repository<Post> PostRepository
<div class="row">
<div class="col-md-2 col-12 mb-5">
<CommunityNavigation Index="1"/>
@if (IdentityService.Permissions[Permission.AdminCommunity])
{
<div class="card card-body mt-5">
<button @onclick="() => CreateModal.Show()" class="btn btn-success">Create new post</button>
</div>
}
</div>
<div class="col-md-10 col-12">
<div class="card border-primary mb-5">
<div class="card-body fs-5">
Planned events and current happenings can be found here.
If you want to know what will happen in the future or is going on now have a look at the posts below
</div>
</div>
<LazyLoader @ref="LazyLoader" Load="Load">
@foreach (var post in Posts)
{
<PostView Post="post" OnUpdate="() => LazyLoader.Reload()"/>
<div class="mb-10"></div>
}
</LazyLoader>
</div>
</div>
@if (IdentityService.Permissions[Permission.AdminCommunity])
{
<CreatePostModal @ref="CreateModal" OnUpdate="() => LazyLoader.Reload()" PostType="PostType.Event"/>
}
@code
{
private LazyLoader LazyLoader;
private CreatePostModal CreateModal;
private Post[] Posts;
private Task Load(LazyLoader _)
{
Posts = PostRepository
.Get()
.Include(x => x.Author)
.Where(x => x.Type == PostType.Event)
.OrderByDescending(x => x.CreatedAt)
.ToArray();
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,67 @@
@page "/community"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities.Community
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Enums
@using Moonlight.App.Models.Enums
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Community
@using Moonlight.Shared.Components.Modals.Community
@inject Repository<Post> PostRepository
@inject IdentityService IdentityService
<div class="row">
<div class="col-md-2 col-12 mb-5">
<CommunityNavigation Index="0"/>
@if (IdentityService.Permissions[Permission.AdminCommunity])
{
<div class="card card-body mt-5">
<button @onclick="() => CreateModal.Show()" class="btn btn-success">Create new post</button>
</div>
}
</div>
<div class="col-md-10 col-12">
<div class="card border-primary mb-5">
<div class="card-body fs-5">
These announcements provide you with the latest news and information.
The posts here have been created by an admin and can contain valuable information
so consider reading it from time to time
</div>
</div>
<LazyLoader @ref="LazyLoader" Load="Load">
@foreach (var post in Posts)
{
<PostView Post="post" OnUpdate="() => LazyLoader.Reload()"/>
<div class="mb-10"></div>
}
</LazyLoader>
</div>
</div>
@if (IdentityService.Permissions[Permission.AdminCommunity])
{
<CreatePostModal @ref="CreateModal" OnUpdate="() => LazyLoader.Reload()" PostType="PostType.Announcement"/>
}
@code
{
private LazyLoader LazyLoader;
private CreatePostModal CreateModal;
private Post[] Posts;
private Task Load(LazyLoader _)
{
Posts = PostRepository
.Get()
.Include(x => x.Author)
.Where(x => x.Type == PostType.Announcement)
.OrderByDescending(x => x.CreatedAt)
.ToArray();
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,58 @@
@page "/community/projects"
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities.Community
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Enums
@using Moonlight.Shared.Components.Community
@using Moonlight.Shared.Components.Modals.Community
@inject Repository<Post> PostRepository
<div class="row">
<div class="col-md-2 col-12 mb-5">
<CommunityNavigation Index="2"/>
<div class="card card-body mt-5">
<button @onclick="() => CreateModal.Show()" class="btn btn-success">Create new post</button>
</div>
</div>
<div class="col-md-10 col-12">
<div class="card border-primary mb-5">
<div class="card-body fs-5">
You have a interesting project or a fun game server you want to share with the community?
You can share it here. Please keep in mind to follow basic rules and dont offend anyone.
Be nice and respectful
</div>
</div>
<LazyLoader @ref="LazyLoader" Load="Load">
@foreach (var post in Posts)
{
<PostView Post="post" OnUpdate="() => LazyLoader.Reload()"/>
<div class="mb-10"></div>
}
</LazyLoader>
</div>
</div>
<CreatePostModal @ref="CreateModal" OnUpdate="() => LazyLoader.Reload()" PostType="PostType.Project"/>
@code
{
private LazyLoader LazyLoader;
private CreatePostModal CreateModal;
private Post[] Posts;
private Task Load(LazyLoader _)
{
Posts = PostRepository
.Get()
.Include(x => x.Author)
.Where(x => x.Type == PostType.Project)
.OrderByDescending(x => x.CreatedAt)
.ToArray();
return Task.CompletedTask;
}
}

View file

@ -18,4 +18,8 @@
.blur {
filter: blur(5px);
}
.ck-powered-by {
display: none;
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

7
Moonlight/wwwroot/js/ckeditor.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -23,9 +23,12 @@ window.moonlight = {
}
},
modals: {
show: function (id)
show: function (id, focus)
{
let modal = new bootstrap.Modal(document.getElementById(id));
let modal = new bootstrap.Modal(document.getElementById(id), {
focus: focus
});
modal.show();
},
hide: function (id)
@ -105,5 +108,34 @@ window.moonlight = {
return text;
}
},
textEditor: {
create: function(id)
{
BalloonEditor
.create(document.getElementById(id), {
toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote' ],
heading: {
options: [
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }
]
}
})
.catch(error => {
console.error(error);
});
},
get: function (id)
{
let editor = document.getElementById(id).ckeditorInstance;
return editor.getData();
},
set: function (id, data)
{
let editor = document.getElementById(id).ckeditorInstance;
editor.setData(data);
}
}
}