Implemented basic store and store admin system. Added alerts. Added db models for store

This commit is contained in:
Marcel Baumgartner
2023-10-18 21:09:14 +02:00
parent 4159170244
commit f7a16fd287
39 changed files with 3252 additions and 1 deletions

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

View 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();
}
}
}

View 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);
}
}

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

View 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();
}
}