Implemented basic store and store admin system. Added alerts. Added db models for store
This commit is contained in:
@@ -6,6 +6,12 @@
|
|||||||
<entry key="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
<entry key="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
|
<option name="solutionLevelOptions">
|
||||||
|
<map>
|
||||||
|
<entry key="migrationsProject" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||||
|
<entry key="startupProject" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
<option name="startupToMigrationsProjects">
|
<option name="startupToMigrationsProjects">
|
||||||
<map>
|
<map>
|
||||||
<entry key="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
<entry key="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" value="691e5ec2-4b4f-4bd1-9cbc-7d3c6efc12da" />
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ public class ConfigV1
|
|||||||
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
|
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
|
||||||
[JsonProperty("MailServer")] public MailServerData MailServer { get; set; } = new();
|
[JsonProperty("MailServer")] public MailServerData MailServer { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonProperty("Store")] public StoreData Store { get; set; } = new();
|
||||||
|
|
||||||
|
public class StoreData
|
||||||
|
{
|
||||||
|
[JsonProperty("Currency")]
|
||||||
|
[Description("A string value representing the currency which will be shown to a user")]
|
||||||
|
public string Currency { get; set; } = "€";
|
||||||
|
}
|
||||||
|
|
||||||
public class SecurityData
|
public class SecurityData
|
||||||
{
|
{
|
||||||
[JsonProperty("Token")]
|
[JsonProperty("Token")]
|
||||||
|
|||||||
@@ -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.Store;
|
||||||
using Moonlight.App.Database.Entities.Tickets;
|
using Moonlight.App.Database.Entities.Tickets;
|
||||||
using Moonlight.App.Services;
|
using Moonlight.App.Services;
|
||||||
|
|
||||||
@@ -13,6 +14,18 @@ public class DataContext : DbContext
|
|||||||
//public DbSet<Ticket> Tickets { get; set; }
|
//public DbSet<Ticket> Tickets { get; set; }
|
||||||
//public DbSet<TicketMessage> TicketMessages { get; set; }
|
//public DbSet<TicketMessage> TicketMessages { get; set; }
|
||||||
|
|
||||||
|
// Store
|
||||||
|
public DbSet<Category> Categories { get; set; }
|
||||||
|
public DbSet<Product> Products { get; set; }
|
||||||
|
public DbSet<Service> Services { get; set; }
|
||||||
|
public DbSet<ServiceShare> ServiceShares { get; set; }
|
||||||
|
|
||||||
|
public DbSet<GiftCode> GiftCodes { get; set; }
|
||||||
|
public DbSet<GiftCodeUse> GiftCodeUses { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Coupon> Coupons { get; set; }
|
||||||
|
public DbSet<CouponUse> CouponUses { get; set; }
|
||||||
|
|
||||||
public DataContext(ConfigService configService)
|
public DataContext(ConfigService configService)
|
||||||
{
|
{
|
||||||
ConfigService = configService;
|
ConfigService = configService;
|
||||||
|
|||||||
9
Moonlight/App/Database/Entities/Store/Category.cs
Normal file
9
Moonlight/App/Database/Entities/Store/Category.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class Category
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
}
|
||||||
9
Moonlight/App/Database/Entities/Store/Coupon.cs
Normal file
9
Moonlight/App/Database/Entities/Store/Coupon.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class Coupon
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Code { get; set; } = "";
|
||||||
|
public int Percent { get; set; }
|
||||||
|
public int Amount { get; set; }
|
||||||
|
}
|
||||||
7
Moonlight/App/Database/Entities/Store/CouponUse.cs
Normal file
7
Moonlight/App/Database/Entities/Store/CouponUse.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class CouponUse
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Coupon Coupon { get; set; }
|
||||||
|
}
|
||||||
9
Moonlight/App/Database/Entities/Store/GiftCode.cs
Normal file
9
Moonlight/App/Database/Entities/Store/GiftCode.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class GiftCode
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Code { get; set; } = "";
|
||||||
|
public double Value { get; set; }
|
||||||
|
public int Amount { get; set; }
|
||||||
|
}
|
||||||
7
Moonlight/App/Database/Entities/Store/GiftCodeUse.cs
Normal file
7
Moonlight/App/Database/Entities/Store/GiftCodeUse.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class GiftCodeUse
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public GiftCode GiftCode { get; set; }
|
||||||
|
}
|
||||||
23
Moonlight/App/Database/Entities/Store/Product.cs
Normal file
23
Moonlight/App/Database/Entities/Store/Product.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class Product
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Category Category { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
|
||||||
|
public double Price { get; set; }
|
||||||
|
public int Stock { get; set; }
|
||||||
|
public int MaxPerUser { get; set; }
|
||||||
|
public int Duration { get; set; }
|
||||||
|
|
||||||
|
public ProductType Type { get; set; }
|
||||||
|
public string ConfigJson { get; set; } = "{}";
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
18
Moonlight/App/Database/Entities/Store/Service.cs
Normal file
18
Moonlight/App/Database/Entities/Store/Service.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class Service
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Nickname { get; set; }
|
||||||
|
|
||||||
|
public bool Suspended { get; set; } = false;
|
||||||
|
|
||||||
|
public Product Product { get; set; }
|
||||||
|
public string? ConfigJsonOverride { get; set; }
|
||||||
|
|
||||||
|
public User Owner { get; set; }
|
||||||
|
public List<ServiceShare> Shares { get; set; } = new();
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime RenewAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
7
Moonlight/App/Database/Entities/Store/ServiceShare.cs
Normal file
7
Moonlight/App/Database/Entities/Store/ServiceShare.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities.Store;
|
||||||
|
|
||||||
|
public class ServiceShare
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public User User { get; set; }
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ public class User
|
|||||||
public string? Avatar { get; set; } = null;
|
public string? Avatar { get; set; } = null;
|
||||||
public string? TotpKey { get; set; } = null;
|
public string? TotpKey { get; set; } = null;
|
||||||
|
|
||||||
|
// Store
|
||||||
|
|
||||||
// Meta data
|
// Meta data
|
||||||
public string Flags { get; set; } = "";
|
public string Flags { get; set; } = "";
|
||||||
public int Permissions { get; set; } = 0;
|
public int Permissions { get; set; } = 0;
|
||||||
|
|||||||
9
Moonlight/App/Database/Enums/ProductType.cs
Normal file
9
Moonlight/App/Database/Enums/ProductType.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Database.Enums;
|
||||||
|
|
||||||
|
public enum ProductType
|
||||||
|
{
|
||||||
|
Server,
|
||||||
|
Webspace,
|
||||||
|
Database,
|
||||||
|
Domain
|
||||||
|
}
|
||||||
341
Moonlight/App/Database/Migrations/20231017075519_AddStoreModels.Designer.cs
generated
Normal file
341
Moonlight/App/Database/Migrations/20231017075519_AddStoreModels.Designer.cs
generated
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
// <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("20231017075519_AddStoreModels")]
|
||||||
|
partial class AddStoreModels
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
|
||||||
|
|
||||||
|
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.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CouponId");
|
||||||
|
|
||||||
|
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.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GiftCodeId");
|
||||||
|
|
||||||
|
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.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Avatar")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
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.Store.CouponUse", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CouponId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
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.Navigation("GiftCode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId");
|
||||||
|
|
||||||
|
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.Service", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shares");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddStoreModels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Categories",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Slug = table.Column<string>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Coupons",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Code = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Percent = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Amount = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Coupons", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "GiftCodes",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Code = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Value = table.Column<double>(type: "REAL", nullable: false),
|
||||||
|
Amount = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_GiftCodes", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Products",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
CategoryId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
|
Name = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Slug = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Price = table.Column<double>(type: "REAL", nullable: false),
|
||||||
|
Stock = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
MaxPerUser = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Duration = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
ConfigJson = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Products", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Products_Categories_CategoryId",
|
||||||
|
column: x => x.CategoryId,
|
||||||
|
principalTable: "Categories",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CouponUses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
CouponId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_CouponUses", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_CouponUses_Coupons_CouponId",
|
||||||
|
column: x => x.CouponId,
|
||||||
|
principalTable: "Coupons",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "GiftCodeUses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
GiftCodeId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_GiftCodeUses", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_GiftCodeUses_GiftCodes_GiftCodeId",
|
||||||
|
column: x => x.GiftCodeId,
|
||||||
|
principalTable: "GiftCodes",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Services",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Nickname = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Suspended = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
ProductId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
ConfigJsonOverride = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
OwnerId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
RenewAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Services", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Services_Products_ProductId",
|
||||||
|
column: x => x.ProductId,
|
||||||
|
principalTable: "Products",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Services_Users_OwnerId",
|
||||||
|
column: x => x.OwnerId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ServiceShares",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
ServiceId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ServiceShares", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ServiceShares_Services_ServiceId",
|
||||||
|
column: x => x.ServiceId,
|
||||||
|
principalTable: "Services",
|
||||||
|
principalColumn: "Id");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ServiceShares_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_CouponUses_CouponId",
|
||||||
|
table: "CouponUses",
|
||||||
|
column: "CouponId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_GiftCodeUses_GiftCodeId",
|
||||||
|
table: "GiftCodeUses",
|
||||||
|
column: "GiftCodeId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Products_CategoryId",
|
||||||
|
table: "Products",
|
||||||
|
column: "CategoryId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Services_OwnerId",
|
||||||
|
table: "Services",
|
||||||
|
column: "OwnerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Services_ProductId",
|
||||||
|
table: "Services",
|
||||||
|
column: "ProductId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServiceShares_ServiceId",
|
||||||
|
table: "ServiceShares",
|
||||||
|
column: "ServiceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ServiceShares_UserId",
|
||||||
|
table: "ServiceShares",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CouponUses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "GiftCodeUses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ServiceShares");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Coupons");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "GiftCodes");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Services");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Products");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Categories");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,210 @@ 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.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.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CouponId");
|
||||||
|
|
||||||
|
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.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GiftCodeId");
|
||||||
|
|
||||||
|
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.User", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -58,6 +262,76 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
|
|
||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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.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.Navigation("GiftCode");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CategoryId");
|
||||||
|
|
||||||
|
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.Service", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Shares");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ public static class Formatter
|
|||||||
return builder =>
|
return builder =>
|
||||||
{
|
{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
var arr = content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var arr = content.Split("\n");
|
||||||
|
|
||||||
foreach (var line in arr)
|
foreach (var line in arr)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public enum Permission
|
|||||||
AdminSessions = 1002,
|
AdminSessions = 1002,
|
||||||
AdminUsersEdit = 1003,
|
AdminUsersEdit = 1003,
|
||||||
AdminTickets = 1004,
|
AdminTickets = 1004,
|
||||||
|
AdminStore = 1900,
|
||||||
AdminViewExceptions = 1999,
|
AdminViewExceptions = 1999,
|
||||||
AdminRoot = 2000
|
AdminRoot = 2000
|
||||||
}
|
}
|
||||||
15
Moonlight/App/Models/Forms/Store/AddCategoryForm.cs
Normal file
15
Moonlight/App/Models/Forms/Store/AddCategoryForm.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms.Store;
|
||||||
|
|
||||||
|
public class AddCategoryForm
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a slug")]
|
||||||
|
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
}
|
||||||
36
Moonlight/App/Models/Forms/Store/AddProductForm.cs
Normal file
36
Moonlight/App/Models/Forms/Store/AddProductForm.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms.Store;
|
||||||
|
|
||||||
|
public class AddProductForm
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a category")]
|
||||||
|
public Category Category { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a description")]
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a slug")]
|
||||||
|
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or above 0")]
|
||||||
|
public double Price { get; set; }
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or above 0")]
|
||||||
|
public int Stock { get; set; }
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or above 0")]
|
||||||
|
public int MaxPerUser { get; set; }
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
|
||||||
|
public int Duration { get; set; }
|
||||||
|
|
||||||
|
public ProductType Type { get; set; }
|
||||||
|
public string ConfigJson { get; set; } = "{}";
|
||||||
|
}
|
||||||
15
Moonlight/App/Models/Forms/Store/EditCategoryForm.cs
Normal file
15
Moonlight/App/Models/Forms/Store/EditCategoryForm.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms.Store;
|
||||||
|
|
||||||
|
public class EditCategoryForm
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a slug")]
|
||||||
|
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
}
|
||||||
36
Moonlight/App/Models/Forms/Store/EditProductForm.cs
Normal file
36
Moonlight/App/Models/Forms/Store/EditProductForm.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms.Store;
|
||||||
|
|
||||||
|
public class EditProductForm
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "You need to specify a category")]
|
||||||
|
public Category Category { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a description")]
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "You need to specify a slug")]
|
||||||
|
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||||
|
public string Slug { get; set; } = "";
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or above 0")]
|
||||||
|
public double Price { get; set; }
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or above 0")]
|
||||||
|
public int Stock { get; set; }
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or above 0")]
|
||||||
|
public int MaxPerUser { get; set; }
|
||||||
|
|
||||||
|
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
|
||||||
|
public int Duration { get; set; }
|
||||||
|
|
||||||
|
public ProductType Type { get; set; }
|
||||||
|
public string ConfigJson { get; set; } = "{}";
|
||||||
|
}
|
||||||
50
Moonlight/App/Services/Interop/AlertService.cs
Normal file
50
Moonlight/App/Services/Interop/AlertService.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Interop;
|
||||||
|
|
||||||
|
public class AlertService
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime JsRuntime;
|
||||||
|
|
||||||
|
public AlertService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
JsRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Info(string title, string message)
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("moonlight.alerts.info", title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Success(string title, string message)
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("moonlight.alerts.success", title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Warning(string title, string message)
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("moonlight.alerts.warning", title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Error(string title, string message)
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("moonlight.alerts.error", title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> Text(string title, string message)
|
||||||
|
{
|
||||||
|
return await JsRuntime.InvokeAsync<string>("moonlight.alerts.text", title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> YesNo(string title, string yes, string no)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await JsRuntime.InvokeAsync<bool>("moonlight.alerts.yesno", title, yes, no);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Moonlight/App/Services/Interop/ModalService.cs
Normal file
37
Moonlight/App/Services/Interop/ModalService.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Interop;
|
||||||
|
|
||||||
|
public class ModalService
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime JsRuntime;
|
||||||
|
|
||||||
|
public ModalService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
JsRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Show(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("moonlight.modals.show", id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Hide(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await JsRuntime.InvokeVoidAsync("moonlight.modals.hide", id);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
Moonlight/App/Services/Store/StoreAdminService.cs
Normal file
99
Moonlight/App/Services/Store/StoreAdminService.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using Moonlight.App.Database.Entities.Store;
|
||||||
|
using Moonlight.App.Database.Enums;
|
||||||
|
using Moonlight.App.Exceptions;
|
||||||
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Services.Store;
|
||||||
|
|
||||||
|
public class StoreAdminService
|
||||||
|
{
|
||||||
|
private readonly Repository<Product> ProductRepository;
|
||||||
|
private readonly Repository<Category> CategoryRepository;
|
||||||
|
|
||||||
|
public StoreAdminService(Repository<Product> productRepository, Repository<Category> categoryRepository)
|
||||||
|
{
|
||||||
|
ProductRepository = productRepository;
|
||||||
|
CategoryRepository = categoryRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Category> AddCategory(string name, string description, string slug)
|
||||||
|
{
|
||||||
|
if (CategoryRepository.Get().Any(x => x.Slug == slug))
|
||||||
|
throw new DisplayException("A category with this slug does already exist");
|
||||||
|
|
||||||
|
var result = CategoryRepository.Add(new Category()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
Slug = slug
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Product> AddProduct(string name, string description, string slug, ProductType type, string configJson,
|
||||||
|
Action<Product>? modifyProduct = null)
|
||||||
|
{
|
||||||
|
if (ProductRepository.Get().Any(x => x.Slug == slug))
|
||||||
|
throw new DisplayException("A product with that slug does already exist");
|
||||||
|
|
||||||
|
var product = new Product()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = description,
|
||||||
|
Slug = slug,
|
||||||
|
Type = type,
|
||||||
|
ConfigJson = configJson
|
||||||
|
};
|
||||||
|
|
||||||
|
if(modifyProduct != null)
|
||||||
|
modifyProduct.Invoke(product);
|
||||||
|
|
||||||
|
var result = ProductRepository.Add(product);
|
||||||
|
|
||||||
|
return Task.FromResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateCategory(Category category)
|
||||||
|
{
|
||||||
|
if (CategoryRepository.Get().Any(x => x.Id != category.Id && x.Slug == category.Slug))
|
||||||
|
throw new DisplayException("A category with that slug does already exist");
|
||||||
|
|
||||||
|
CategoryRepository.Update(category);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdateProduct(Product product)
|
||||||
|
{
|
||||||
|
if (ProductRepository.Get().Any(x => x.Id != product.Id && x.Slug == product.Slug))
|
||||||
|
throw new DisplayException("A product with that slug does already exist");
|
||||||
|
|
||||||
|
ProductRepository.Update(product);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteCategory(Category category)
|
||||||
|
{
|
||||||
|
var hasProductsInIt = ProductRepository
|
||||||
|
.Get()
|
||||||
|
.Any(x => x.Category!.Id == category.Id);
|
||||||
|
|
||||||
|
if (hasProductsInIt)
|
||||||
|
throw new DisplayException("The category contains products. You need to delete the products in order to delete the category");
|
||||||
|
|
||||||
|
CategoryRepository.Delete(category);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteProduct(Product product)
|
||||||
|
{
|
||||||
|
//TODO: Implement checks if services with that product id exist
|
||||||
|
|
||||||
|
ProductRepository.Delete(product);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Moonlight/App/Services/Store/StoreService.cs
Normal file
13
Moonlight/App/Services/Store/StoreService.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Moonlight.App.Services.Store;
|
||||||
|
|
||||||
|
public class StoreService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider ServiceProvider;
|
||||||
|
|
||||||
|
public StoreAdminService Admin => ServiceProvider.GetRequiredService<StoreAdminService>();
|
||||||
|
|
||||||
|
public StoreService(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
ServiceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
<PackageReference Include="BlazorTable" Version="1.17.0" />
|
||||||
<PackageReference Include="JWT" Version="10.1.1" />
|
<PackageReference Include="JWT" Version="10.1.1" />
|
||||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||||
|
<PackageReference Include="Mappy.Net" Version="1.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
|
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
|
||||||
|
|
||||||
<link href="/css/theme.css" rel="stylesheet" type="text/css"/>
|
<link href="/css/theme.css" rel="stylesheet" type="text/css"/>
|
||||||
|
<link href="/css/utils.css" rel="stylesheet" type="text/css"/>
|
||||||
<link href="/css/blazor.css" rel="stylesheet" type="text/css"/>
|
<link href="/css/blazor.css" rel="stylesheet" type="text/css"/>
|
||||||
<link href="/css/boxicons.min.css" rel="stylesheet" type="text/css"/>
|
<link href="/css/boxicons.min.css" rel="stylesheet" type="text/css"/>
|
||||||
|
<link href="/css/sweetalert2dark.css" rel="stylesheet" type="text/css"/>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css">
|
<link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css">
|
||||||
</head>
|
</head>
|
||||||
<body data-kt-app-header-fixed="true"
|
<body data-kt-app-header-fixed="true"
|
||||||
@@ -34,6 +36,7 @@
|
|||||||
<script src="/js/toaster.js"></script>
|
<script src="/js/toaster.js"></script>
|
||||||
<script src="/js/moonlight.js"></script>
|
<script src="/js/moonlight.js"></script>
|
||||||
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
|
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
|
||||||
|
<script src="/js/sweetalert2.js"></script>
|
||||||
<script src="/_framework/blazor.server.js"></script>
|
<script src="/_framework/blazor.server.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -7,6 +7,7 @@ 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.Interop;
|
using Moonlight.App.Services.Interop;
|
||||||
|
using Moonlight.App.Services.Store;
|
||||||
using Moonlight.App.Services.Users;
|
using Moonlight.App.Services.Users;
|
||||||
using Moonlight.App.Services.Utils;
|
using Moonlight.App.Services.Utils;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -36,6 +37,12 @@ builder.Services.AddScoped<JwtService>();
|
|||||||
// Services / Interop
|
// Services / Interop
|
||||||
builder.Services.AddScoped<CookieService>();
|
builder.Services.AddScoped<CookieService>();
|
||||||
builder.Services.AddScoped<ToastService>();
|
builder.Services.AddScoped<ToastService>();
|
||||||
|
builder.Services.AddScoped<ModalService>();
|
||||||
|
builder.Services.AddScoped<AlertService>();
|
||||||
|
|
||||||
|
// Services / Store
|
||||||
|
builder.Services.AddScoped<StoreService>();
|
||||||
|
builder.Services.AddScoped<StoreAdminService>();
|
||||||
|
|
||||||
// Services / Users
|
// Services / Users
|
||||||
builder.Services.AddScoped<UserService>();
|
builder.Services.AddScoped<UserService>();
|
||||||
|
|||||||
27
Moonlight/Shared/Components/Forms/SmartEnumSelect.razor
Normal file
27
Moonlight/Shared/Components/Forms/SmartEnumSelect.razor
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@typeparam TField where TField : struct
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@inherits InputBase<TField>
|
||||||
|
|
||||||
|
<select @bind="CurrentValue" class="form-select">
|
||||||
|
@foreach (var status in (TField[])Enum.GetValues(typeof(TField)))
|
||||||
|
{
|
||||||
|
if (CurrentValue.ToString() == status.ToString())
|
||||||
|
{
|
||||||
|
<option value="@(status)" selected="">@(status)</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<option value="@(status)">@(status)</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
protected override bool TryParseValueFromString(string? value, out TField result, out string? validationErrorMessage)
|
||||||
|
{
|
||||||
|
result = Enum.Parse<TField>(value);
|
||||||
|
validationErrorMessage = "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Moonlight/Shared/Components/Forms/SmartSelect.razor
Normal file
71
Moonlight/Shared/Components/Forms/SmartSelect.razor
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
@typeparam TField
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@inherits InputBase<TField>
|
||||||
|
|
||||||
|
<select class="form-select" @bind="Binding">
|
||||||
|
@if (CanBeNull)
|
||||||
|
{
|
||||||
|
<option value="-1">---</option>
|
||||||
|
}
|
||||||
|
@foreach (var item in Items)
|
||||||
|
{
|
||||||
|
<option value="@(item!.GetHashCode())">@(DisplayField(item))</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<TField> Items { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<TField, string> DisplayField { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<Task>? OnChange { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool CanBeNull { get; set; } = false;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string? FormatValueAsString(TField? value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return DisplayField.Invoke(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool TryParseValueFromString(string? value, out TField result, out string? validationErrorMessage)
|
||||||
|
{
|
||||||
|
validationErrorMessage = "";
|
||||||
|
result = default(TField)!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Binding
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Value == null)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return Value.GetHashCode();
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var i = Items.FirstOrDefault(x => x!.GetHashCode() == value);
|
||||||
|
|
||||||
|
if(i == null && !CanBeNull)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Value = i;
|
||||||
|
ValueChanged.InvokeAsync(i);
|
||||||
|
OnChange?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Moonlight/Shared/Components/Partials/SmartModal.razor
Normal file
35
Moonlight/Shared/Components/Partials/SmartModal.razor
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@inject ModalService ModalService
|
||||||
|
|
||||||
|
<div class="modal fade" id="modal@(Id)" tabindex="-1">
|
||||||
|
<div class="modal-dialog @(CssClasses)">
|
||||||
|
<div class="modal-content">
|
||||||
|
@ChildContent
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string CssClasses { get; set; } = "";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment ChildContent { get; set; }
|
||||||
|
|
||||||
|
private int Id;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Id = GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Show()
|
||||||
|
{
|
||||||
|
await ModalService.Show("modal" + Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Hide()
|
||||||
|
{
|
||||||
|
await ModalService.Hide("modal" + Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
316
Moonlight/Shared/Components/Store/StoreModals.razor
Normal file
316
Moonlight/Shared/Components/Store/StoreModals.razor
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
@using Moonlight.App.Services.Store
|
||||||
|
@using Moonlight.App.Models.Forms.Store
|
||||||
|
@using Moonlight.App.Database.Enums
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Mappy.Net
|
||||||
|
|
||||||
|
@inject StoreService StoreService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject Repository<Category> CategoryRepository
|
||||||
|
|
||||||
|
<SmartModal @ref="AddCategoryModal" CssClasses="modal-dialog-centered">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add new category</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<SmartForm Model="AddCategoryForm" OnValidSubmit="AddCategorySubmit">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input @bind="AddCategoryForm.Name" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea @bind="AddCategoryForm.Description" class="form-control" type="text"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Slug</label>
|
||||||
|
<input @bind="AddCategoryForm.Slug" class="form-control" type="text"/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<SmartModal @ref="EditCategoryModal" CssClasses="modal-dialog-centered">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Edit category</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<SmartForm Model="EditCategoryForm" OnValidSubmit="EditCategorySubmit">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input @bind="EditCategoryForm.Name" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea @bind="EditCategoryForm.Description" class="form-control" type="text"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Slug</label>
|
||||||
|
<input @bind="EditCategoryForm.Slug" class="form-control" type="text"/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<SmartModal @ref="AddProductModal" CssClasses="modal-dialog-centered modal-lg">
|
||||||
|
<LazyLoader Load="LoadCategories">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add new product</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<SmartForm Model="AddProductForm" OnValidSubmit="AddProductSubmit">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input @bind="AddProductForm.Name" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea @bind="AddProductForm.Description" class="form-control" type="text"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Slug</label>
|
||||||
|
<input @bind="AddProductForm.Slug" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Category</label>
|
||||||
|
<SmartSelect @bind-Value="AddProductForm.Category" Items="Categories" DisplayField="@(x => x.Name)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Price</label>
|
||||||
|
<input @bind="AddProductForm.Price" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Duration</label>
|
||||||
|
<input @bind="AddProductForm.Duration" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Max instances per user</label>
|
||||||
|
<input @bind="AddProductForm.MaxPerUser" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Stock</label>
|
||||||
|
<input @bind="AddProductForm.Stock" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<SmartEnumSelect @bind-Value="AddProductForm.Type"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Config</label>
|
||||||
|
<input @bind="AddProductForm.ConfigJson" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</LazyLoader>
|
||||||
|
</SmartModal>
|
||||||
|
|
||||||
|
<SmartModal @ref="EditProductModal" CssClasses="modal-dialog-centered modal-lg">
|
||||||
|
<LazyLoader Load="LoadCategories">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Edit product</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<SmartForm Model="EditProductForm" OnValidSubmit="EditProductSubmit">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Name</label>
|
||||||
|
<input @bind="EditProductForm.Name" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Description</label>
|
||||||
|
<textarea @bind="EditProductForm.Description" class="form-control" type="text"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Slug</label>
|
||||||
|
<input @bind="EditProductForm.Slug" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="form-label">Category</label>
|
||||||
|
<SmartSelect @bind-Value="EditProductForm.Category" Items="Categories" DisplayField="@(x => x.Name)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Price</label>
|
||||||
|
<input @bind="EditProductForm.Price" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Duration</label>
|
||||||
|
<input @bind="EditProductForm.Duration" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Max instances per user</label>
|
||||||
|
<input @bind="EditProductForm.MaxPerUser" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Stock</label>
|
||||||
|
<input @bind="EditProductForm.Stock" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<SmartEnumSelect @bind-Value="EditProductForm.Type"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Config</label>
|
||||||
|
<input @bind="EditProductForm.ConfigJson" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</LazyLoader>
|
||||||
|
</SmartModal>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public Func<Task> OnUpdate { get; set; }
|
||||||
|
|
||||||
|
#region Add category
|
||||||
|
|
||||||
|
private SmartModal AddCategoryModal;
|
||||||
|
private AddCategoryForm AddCategoryForm = new();
|
||||||
|
|
||||||
|
public Task AddCategoryShow => AddCategoryModal.Show();
|
||||||
|
|
||||||
|
private async Task AddCategorySubmit()
|
||||||
|
{
|
||||||
|
await StoreService.Admin.AddCategory(
|
||||||
|
AddCategoryForm.Name,
|
||||||
|
AddCategoryForm.Description,
|
||||||
|
AddCategoryForm.Slug
|
||||||
|
);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully added category");
|
||||||
|
await AddCategoryModal.Hide();
|
||||||
|
|
||||||
|
AddCategoryForm = new();
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Edit category
|
||||||
|
|
||||||
|
private SmartModal EditCategoryModal;
|
||||||
|
private EditCategoryForm EditCategoryForm = new();
|
||||||
|
private Category EditCategory;
|
||||||
|
|
||||||
|
public async Task EditCategoryShow(Category category)
|
||||||
|
{
|
||||||
|
EditCategory = category;
|
||||||
|
|
||||||
|
EditCategoryForm = Mapper.Map<EditCategoryForm>(EditCategory);
|
||||||
|
await EditCategoryModal.Show();
|
||||||
|
}
|
||||||
|
private async Task EditCategorySubmit()
|
||||||
|
{
|
||||||
|
EditCategory = Mapper.Map(EditCategory, EditCategoryForm);
|
||||||
|
|
||||||
|
await StoreService.Admin.UpdateCategory(EditCategory);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully updated category");
|
||||||
|
await EditCategoryModal.Hide();
|
||||||
|
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Add product
|
||||||
|
|
||||||
|
private SmartModal AddProductModal;
|
||||||
|
private AddProductForm AddProductForm = new();
|
||||||
|
private Category[] Categories;
|
||||||
|
|
||||||
|
public Task AddProductShow => AddProductModal.Show();
|
||||||
|
|
||||||
|
private async Task AddProductSubmit()
|
||||||
|
{
|
||||||
|
await StoreService.Admin.AddProduct(
|
||||||
|
AddProductForm.Name,
|
||||||
|
AddProductForm.Description,
|
||||||
|
AddProductForm.Slug,
|
||||||
|
AddProductForm.Type,
|
||||||
|
AddProductForm.ConfigJson,
|
||||||
|
product =>
|
||||||
|
{
|
||||||
|
product.Category = AddProductForm.Category;
|
||||||
|
product.Duration = AddProductForm.Duration;
|
||||||
|
product.Price = AddProductForm.Price;
|
||||||
|
product.Stock = AddProductForm.Stock;
|
||||||
|
product.MaxPerUser = AddProductForm.MaxPerUser;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully added product");
|
||||||
|
await AddProductModal.Hide();
|
||||||
|
|
||||||
|
AddProductForm = new();
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Edit product
|
||||||
|
|
||||||
|
private SmartModal EditProductModal;
|
||||||
|
private EditProductForm EditProductForm = new();
|
||||||
|
private Product EditProduct;
|
||||||
|
|
||||||
|
public async Task EditProductShow(Product product)
|
||||||
|
{
|
||||||
|
EditProduct = product;
|
||||||
|
|
||||||
|
EditProductForm = Mapper.Map<EditProductForm>(EditProduct);
|
||||||
|
await EditProductModal.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditProductSubmit()
|
||||||
|
{
|
||||||
|
EditProduct = Mapper.Map(EditProduct, EditProductForm);
|
||||||
|
|
||||||
|
await StoreService.Admin.UpdateProduct(EditProduct);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully updated product");
|
||||||
|
await EditProductModal.Hide();
|
||||||
|
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private Task LoadCategories(LazyLoader _)
|
||||||
|
{
|
||||||
|
Categories = CategoryRepository.Get().ToArray();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
263
Moonlight/Shared/Views/Store/Index.razor
Normal file
263
Moonlight/Shared/Views/Store/Index.razor
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
@page "/store"
|
||||||
|
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Moonlight.App.Models.Enums
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Services.Store
|
||||||
|
@using Moonlight.Shared.Components.Store
|
||||||
|
|
||||||
|
@inject Repository<Category> CategoryRepository
|
||||||
|
@inject Repository<Product> ProductRepository
|
||||||
|
@inject ConfigService ConfigService
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
@inject AlertService AlertService
|
||||||
|
@inject ToastService ToastService
|
||||||
|
@inject StoreService StoreService
|
||||||
|
|
||||||
|
@{
|
||||||
|
var currency = ConfigService.Get().Store.Currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (IdentityService.Permissions[Permission.AdminStore])
|
||||||
|
{
|
||||||
|
<div class="alert alert-info bg-info text-white text-center py-2">
|
||||||
|
@if (EditMode)
|
||||||
|
{
|
||||||
|
<h4 class="pt-2">Edit mode enabled. Disable it by clicking <a href="#" @onclick="ToggleEdit" @onclick:preventDefault>here</a></h4>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<h4 class="pt-2">To edit the store you can enable the edit mode <a href="#" @onclick="ToggleEdit" @onclick:preventDefault>here</a></h4>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-12 mb-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="card-title">Categories</h6>
|
||||||
|
@if (EditMode)
|
||||||
|
{
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<button @onclick="() => StoreModals.AddCategoryShow" class="btn btn-icon btn-success">
|
||||||
|
<i class="bx bx-sm bx-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader @ref="CategoriesLazyLoader" Load="LoadCategories">
|
||||||
|
@foreach (var category in Categories)
|
||||||
|
{
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<li class="d-flex align-items-center py-2">
|
||||||
|
<span class="bullet me-5"></span>
|
||||||
|
<a class="invisible-a fs-5 @(SelectedCategory == category ? "fw-bold text-primary" : "")" href="/store?category=@(category.Slug)">@(category.Name)</a>
|
||||||
|
@if (EditMode)
|
||||||
|
{
|
||||||
|
<a @onclick="() => StoreModals.EditCategoryShow(category)" @onclick:preventDefault href="#" class="ms-3 text-warning">Edit</a>
|
||||||
|
<a @onclick="() => DeleteCategory(category)" @onclick:preventDefault href="#" class="ms-1 text-danger">Delete</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 col-12">
|
||||||
|
<LazyLoader @ref="ProductsLazyLoader" Load="LoadProducts">
|
||||||
|
@if (Products.Any())
|
||||||
|
{
|
||||||
|
<div class="row">
|
||||||
|
@foreach (var product in Products)
|
||||||
|
{
|
||||||
|
<div class="col-md-4 col-12 mb-5">
|
||||||
|
<div class="card">
|
||||||
|
@if (EditMode)
|
||||||
|
{
|
||||||
|
<div class="card-header">
|
||||||
|
<a @onclick="() => StoreModals.EditProductShow(product)" @onclick:preventDefault href="#" class="card-title text-primary">Edit</a>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<a @onclick="() => DeleteProduct(product)" @onclick:preventDefault href="#" class="text-danger">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h1 class="text-dark mb-5 fw-bolder">@(product.Name)</h1>
|
||||||
|
<p class="fw-semibold fs-6 text-gray-800 flex-grow-1">
|
||||||
|
@(Formatter.FormatLineBreaks(product.Description))
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
@if (product.Price == 0)
|
||||||
|
{
|
||||||
|
<span class="fs-1 fw-bold text-primary">
|
||||||
|
Free
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="mb-2 text-primary">@(currency)</span>
|
||||||
|
<span class="fs-1 fw-bold text-primary">
|
||||||
|
@(product.Price)
|
||||||
|
</span>
|
||||||
|
<span class="fs-7 fw-semibold opacity-50">
|
||||||
|
/
|
||||||
|
<span>@(product.Duration) days</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (product.Stock == 0)
|
||||||
|
{
|
||||||
|
<button class="btn btn-primary disabled">Out of stock</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a href="/store/order/@(product.Slug)" class="btn btn-primary">Order now</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (EditMode)
|
||||||
|
{
|
||||||
|
<div class="col-md-4 col-12 mb-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<button @onclick="() => StoreModals.AddProductShow" class="btn btn-success">Create new product</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (Categories.Any())
|
||||||
|
{
|
||||||
|
if (EditMode)
|
||||||
|
{
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-10">
|
||||||
|
<button @onclick="() => StoreModals.AddProductShow" class="btn btn-success">Create new product</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="card card-body text-center">
|
||||||
|
<div class="py-10">
|
||||||
|
<h1 class="card-title">Welcome to our store</h1>
|
||||||
|
<span class="card-subtitle fs-2">Select a category to start browsing</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-10 text-center p-10">
|
||||||
|
<img src="/svg/shopping.svg" style="height: 10vi" alt="Banner">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="card card-body text-center">
|
||||||
|
<h1 class="card-title py-10">No products found</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StoreModals @ref="StoreModals" OnUpdate="OnParametersSetAsync" />
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
// Category
|
||||||
|
private Category[] Categories;
|
||||||
|
private LazyLoader? CategoriesLazyLoader;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
[SupplyParameterFromQuery]
|
||||||
|
public string Category { get; set; }
|
||||||
|
|
||||||
|
private Category? SelectedCategory;
|
||||||
|
|
||||||
|
// Products
|
||||||
|
private Product[] Products;
|
||||||
|
private LazyLoader? ProductsLazyLoader;
|
||||||
|
|
||||||
|
// Edit stuff
|
||||||
|
private bool EditMode = false;
|
||||||
|
private StoreModals StoreModals;
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (CategoriesLazyLoader != null)
|
||||||
|
await CategoriesLazyLoader.Reload();
|
||||||
|
|
||||||
|
if (ProductsLazyLoader != null)
|
||||||
|
await ProductsLazyLoader.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleEdit()
|
||||||
|
{
|
||||||
|
EditMode = !EditMode;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task LoadCategories(LazyLoader _)
|
||||||
|
{
|
||||||
|
Categories = CategoryRepository.Get().ToArray();
|
||||||
|
|
||||||
|
SelectedCategory = Categories.FirstOrDefault(x => x.Slug == Category);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task LoadProducts(LazyLoader _)
|
||||||
|
{
|
||||||
|
if (SelectedCategory == null)
|
||||||
|
{
|
||||||
|
Products = ProductRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Category == null)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Products = ProductRepository
|
||||||
|
.Get()
|
||||||
|
.Where(x => x.Category!.Id == SelectedCategory.Id)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteCategory(Category category)
|
||||||
|
{
|
||||||
|
if (!await AlertService.YesNo($"Do you really want to delete '{category.Name}'", "Continue", "Cancel"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await StoreService.Admin.DeleteCategory(category);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully deleted category");
|
||||||
|
await OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DeleteProduct(Product product)
|
||||||
|
{
|
||||||
|
if (!await AlertService.YesNo($"Do you really want to delete '{product.Name}'", "Continue", "Cancel"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await StoreService.Admin.DeleteProduct(product);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully deleted product");
|
||||||
|
await OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
1126
Moonlight/wwwroot/css/sweetalert2dark.css
vendored
Normal file
1126
Moonlight/wwwroot/css/sweetalert2dark.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
Moonlight/wwwroot/css/utils.css
vendored
Normal file
21
Moonlight/wwwroot/css/utils.css
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.invisible-a {
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible-a:hover {
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur-unless-hover {
|
||||||
|
filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur-unless-hover:hover {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur {
|
||||||
|
filter: blur(5px);
|
||||||
|
}
|
||||||
84
Moonlight/wwwroot/js/moonlight.js
vendored
84
Moonlight/wwwroot/js/moonlight.js
vendored
@@ -21,5 +21,89 @@ window.moonlight = {
|
|||||||
var toast = new ToastHelper(title, message, color, timeout);
|
var toast = new ToastHelper(title, message, color, timeout);
|
||||||
toast.show();
|
toast.show();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
modals: {
|
||||||
|
show: function (id)
|
||||||
|
{
|
||||||
|
let modal = new bootstrap.Modal(document.getElementById(id));
|
||||||
|
modal.show();
|
||||||
|
},
|
||||||
|
hide: function (id)
|
||||||
|
{
|
||||||
|
let element = document.getElementById(id)
|
||||||
|
let modal = bootstrap.Modal.getInstance(element)
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
alerts: {
|
||||||
|
getHelper: function(){
|
||||||
|
return Swal.mixin({
|
||||||
|
customClass: {
|
||||||
|
confirmButton: 'btn btn-success',
|
||||||
|
cancelButton: 'btn btn-danger',
|
||||||
|
denyButton: 'btn btn-danger',
|
||||||
|
htmlContainer: 'text-white'
|
||||||
|
},
|
||||||
|
buttonsStyling: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
info: function (title, description) {
|
||||||
|
this.getHelper().fire(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
'info'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
success: function (title, description) {
|
||||||
|
this.getHelper().fire(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
'success'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
warning: function (title, description) {
|
||||||
|
this.getHelper().fire(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
'warning'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
error: function (title, description) {
|
||||||
|
this.getHelper().fire(
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
yesno: function (title, yesText, noText) {
|
||||||
|
return this.getHelper().fire({
|
||||||
|
title: title,
|
||||||
|
showDenyButton: true,
|
||||||
|
confirmButtonText: yesText,
|
||||||
|
denyButtonText: noText,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
return true;
|
||||||
|
} else if (result.isDenied) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
text: function (title, description) {
|
||||||
|
const {value: text} = this.getHelper().fire({
|
||||||
|
title: title,
|
||||||
|
input: 'text',
|
||||||
|
inputLabel: description,
|
||||||
|
inputValue: "",
|
||||||
|
showCancelButton: false,
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return 'You need to enter a value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
Moonlight/wwwroot/js/sweetalert2.js
vendored
Normal file
6
Moonlight/wwwroot/js/sweetalert2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Moonlight/wwwroot/svg/shopping.svg
vendored
Normal file
1
Moonlight/wwwroot/svg/shopping.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
Reference in New Issue
Block a user