Added auto form stuff and started implementing auto crud and store admin things
This commit is contained in:
8
Moonlight/App/Extensions/Attributes/SelectorAttribute.cs
Normal file
8
Moonlight/App/Extensions/Attributes/SelectorAttribute.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Moonlight.App.Extensions.Attributes;
|
||||||
|
|
||||||
|
public class SelectorAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string SelectorProp { get; set; } = "";
|
||||||
|
public string DisplayProp { get; set; } = "";
|
||||||
|
public bool UseDropdown { get; set; } = false;
|
||||||
|
}
|
||||||
12
Moonlight/App/Helpers/ComponentHelper.cs
Normal file
12
Moonlight/App/Helpers/ComponentHelper.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public static class ComponentHelper
|
||||||
|
{
|
||||||
|
public static RenderFragment FromType(Type type) => builder =>
|
||||||
|
{
|
||||||
|
builder.OpenComponent(0, type);
|
||||||
|
builder.CloseComponent();
|
||||||
|
};
|
||||||
|
}
|
||||||
57
Moonlight/App/Helpers/PropBinder.cs
Normal file
57
Moonlight/App/Helpers/PropBinder.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class PropBinder<T>
|
||||||
|
{
|
||||||
|
private PropertyInfo PropertyInfo;
|
||||||
|
private object DataObject;
|
||||||
|
|
||||||
|
public PropBinder(PropertyInfo propertyInfo, object dataObject)
|
||||||
|
{
|
||||||
|
PropertyInfo = propertyInfo;
|
||||||
|
DataObject = dataObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StringValue
|
||||||
|
{
|
||||||
|
get => (string)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int IntValue
|
||||||
|
{
|
||||||
|
get => (int)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long LongValue
|
||||||
|
{
|
||||||
|
get => (long)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BoolValue
|
||||||
|
{
|
||||||
|
get => (bool)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime DateTimeValue
|
||||||
|
{
|
||||||
|
get => (DateTime)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Class
|
||||||
|
{
|
||||||
|
get => (T)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double DoubleValue
|
||||||
|
{
|
||||||
|
get => (double)PropertyInfo.GetValue(DataObject)!;
|
||||||
|
set => PropertyInfo.SetValue(DataObject, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Moonlight/App/Models/Forms/Store/AddCouponForm.cs
Normal file
16
Moonlight/App/Models/Forms/Store/AddCouponForm.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Models.Forms.Store;
|
||||||
|
|
||||||
|
public class AddCouponForm
|
||||||
|
{
|
||||||
|
[MinLength(5, ErrorMessage = "The code needs to be longer than 4")]
|
||||||
|
[MaxLength(15, ErrorMessage = "The code should not be longer than 15 characters")]
|
||||||
|
public string Code { get; set; } = "";
|
||||||
|
|
||||||
|
[Range(1, 99, ErrorMessage = "The percent needs to be between 1 and 99")]
|
||||||
|
public int Percent { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue, ErrorMessage = "The amount needs to be equals or greater than 0")]
|
||||||
|
public int Amount { get; set; }
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Moonlight.App.Database.Entities.Store;
|
using Moonlight.App.Database.Entities.Store;
|
||||||
using Moonlight.App.Database.Enums;
|
using Moonlight.App.Database.Enums;
|
||||||
|
using Moonlight.App.Extensions.Attributes;
|
||||||
|
|
||||||
namespace Moonlight.App.Models.Forms.Store;
|
namespace Moonlight.App.Models.Forms.Store;
|
||||||
|
|
||||||
public class AddProductForm
|
public class AddProductForm
|
||||||
{
|
{
|
||||||
[Required(ErrorMessage = "You need to specify a category")]
|
[Required(ErrorMessage = "You need to specify a category")]
|
||||||
|
[Selector(DisplayProp = "Name", SelectorProp = "Name", UseDropdown = true)]
|
||||||
public Category Category { get; set; }
|
public Category Category { get; set; }
|
||||||
|
|
||||||
[Required(ErrorMessage = "You need to specify a name")]
|
[Required(ErrorMessage = "You need to specify a name")]
|
||||||
|
[Description("Teeeeeeeeeeeeeeeeeeeeeeeeeest")]
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
[Required(ErrorMessage = "You need to specify a description")]
|
[Required(ErrorMessage = "You need to specify a description")]
|
||||||
@@ -19,16 +23,16 @@ public class AddProductForm
|
|||||||
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
[RegularExpression("^[a-z0-9-]+$", ErrorMessage = "You need to enter a valid slug only containing lowercase characters and numbers")]
|
||||||
public string Slug { get; set; } = "";
|
public string Slug { get; set; } = "";
|
||||||
|
|
||||||
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or above 0")]
|
[Range(0, double.MaxValue, ErrorMessage = "The price needs to be equals or greater than 0")]
|
||||||
public double Price { get; set; }
|
public double Price { get; set; }
|
||||||
|
|
||||||
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or above 0")]
|
[Range(0, double.MaxValue, ErrorMessage = "The stock needs to be equals or greater than 0")]
|
||||||
public int Stock { get; set; }
|
public int Stock { get; set; }
|
||||||
|
|
||||||
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or above 0")]
|
[Range(0, double.MaxValue, ErrorMessage = "The max per user amount needs to be equals or greater than 0")]
|
||||||
public int MaxPerUser { get; set; }
|
public int MaxPerUser { get; set; }
|
||||||
|
|
||||||
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
|
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or greater than 0")]
|
||||||
public int Duration { get; set; }
|
public int Duration { get; set; }
|
||||||
|
|
||||||
public ServiceType Type { get; set; }
|
public ServiceType Type { get; set; }
|
||||||
|
|||||||
67
Moonlight/Shared/Components/Forms/AutoCrud.razor
Normal file
67
Moonlight/Shared/Components/Forms/AutoCrud.razor
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@using BlazorTable
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
|
||||||
|
@typeparam TItem where TItem : class
|
||||||
|
|
||||||
|
@inject Repository<TItem> ItemRepository
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">@(Title)</h3>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<button class="btn btn-icon btn-success">
|
||||||
|
<i class="bx bx-sm bx-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader Load="LoadItems">
|
||||||
|
<Table TableItem="TItem"
|
||||||
|
Items="Items"
|
||||||
|
PageSize="50"
|
||||||
|
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||||
|
TableHeadClass="fw-bold text-muted">
|
||||||
|
@ChildContent
|
||||||
|
</Table>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Type CreateForm { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Type UpdateForm { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<Repository<TItem>, TItem[]> Load { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment ChildContent { get; set; }
|
||||||
|
|
||||||
|
private TItem[] Items;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (CreateForm == null)
|
||||||
|
throw new ArgumentNullException(nameof(CreateForm));
|
||||||
|
|
||||||
|
if (UpdateForm == null)
|
||||||
|
throw new ArgumentNullException(nameof(UpdateForm));
|
||||||
|
|
||||||
|
if (Load == null)
|
||||||
|
throw new ArgumentNullException(nameof(Load));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task LoadItems(LazyLoader _)
|
||||||
|
{
|
||||||
|
Items = Load.Invoke(ItemRepository);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Moonlight/Shared/Components/Forms/AutoForm.razor
Normal file
27
Moonlight/Shared/Components/Forms/AutoForm.razor
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
@using Moonlight.App.Extensions
|
||||||
|
|
||||||
|
@typeparam TForm
|
||||||
|
|
||||||
|
@foreach (var prop in typeof(TForm).GetProperties())
|
||||||
|
{
|
||||||
|
<div class="col-md-@(Columns) col-12">
|
||||||
|
<CascadingValue Name="Property" Value="prop">
|
||||||
|
<CascadingValue Name="Data" Value="(object)Model">
|
||||||
|
@{
|
||||||
|
var typeToCreate = typeof(AutoProperty<>).MakeGenericType(prop.PropertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ComponentHelper.FromType(typeToCreate)
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public TForm Model { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int Columns { get; set; } = 6;
|
||||||
|
}
|
||||||
151
Moonlight/Shared/Components/Forms/AutoProperty.razor
Normal file
151
Moonlight/Shared/Components/Forms/AutoProperty.razor
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
@using System.Reflection
|
||||||
|
@using System.ComponentModel
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Moonlight.App.Extensions.Attributes
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
|
||||||
|
@typeparam TProp
|
||||||
|
@inject IServiceProvider ServiceProvider
|
||||||
|
|
||||||
|
<label class="form-label">
|
||||||
|
@(Formatter.ConvertCamelCaseToSpaces(Property.Name))
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@* Description using attribute *@
|
||||||
|
|
||||||
|
@{
|
||||||
|
var attrs = Property.GetCustomAttributes(true);
|
||||||
|
|
||||||
|
var descAttr = attrs
|
||||||
|
.FirstOrDefault(x => x.GetType() == typeof(DescriptionAttribute));
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (descAttr != null)
|
||||||
|
{
|
||||||
|
var attribute = descAttr as DescriptionAttribute;
|
||||||
|
|
||||||
|
<div class="form-text fs-5 mb-2 mt-0">
|
||||||
|
@(attribute!.Description)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* Actual value binding *@
|
||||||
|
|
||||||
|
<div class="input-group mb-5">
|
||||||
|
@if (Property.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
<div class="w-100">
|
||||||
|
<InputText id="@Property.Name" @bind-Value="Binder.StringValue" class="form-control"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType == typeof(int))
|
||||||
|
{
|
||||||
|
<InputNumber id="@Property.Name" @bind-Value="Binder.IntValue" class="form-control"/>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType == typeof(double))
|
||||||
|
{
|
||||||
|
<InputNumber id="@Property.Name" @bind-Value="Binder.DoubleValue" class="form-control"/>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType == typeof(long))
|
||||||
|
{
|
||||||
|
<InputNumber id="@Property.Name" @bind-Value="Binder.LongValue" class="form-control"/>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType == typeof(bool))
|
||||||
|
{
|
||||||
|
<div class="form-check">
|
||||||
|
<InputCheckbox id="@Property.Name" @bind-Value="Binder.BoolValue" class="form-check-input"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType == typeof(DateTime))
|
||||||
|
{
|
||||||
|
<InputDate id="@Property.Name" @bind-Value="Binder.DateTimeValue" class="form-control"/>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType == typeof(decimal))
|
||||||
|
{
|
||||||
|
<InputNumber id="@Property.Name" step="0.01" @bind-Value="Binder.DoubleValue" class="form-control"/>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType.IsEnum)
|
||||||
|
{
|
||||||
|
<select @bind="Binder.Class" class="form-select">
|
||||||
|
@foreach (var status in (TProp[])Enum.GetValues(typeof(TProp)))
|
||||||
|
{
|
||||||
|
if (Binder.Class.ToString() == status.ToString())
|
||||||
|
{
|
||||||
|
<option value="@(status)" selected="">@(status)</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<option value="@(status)">@(status)</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
else if (Property.PropertyType.IsClass)
|
||||||
|
{
|
||||||
|
var attribute = Property.GetCustomAttributes(true)
|
||||||
|
.FirstOrDefault(x => x.GetType() == typeof(SelectorAttribute)) as SelectorAttribute;
|
||||||
|
|
||||||
|
if (attribute != null)
|
||||||
|
{
|
||||||
|
if (attribute.UseDropdown)
|
||||||
|
{
|
||||||
|
var displayFunc = new Func<TProp, string>(x =>
|
||||||
|
{
|
||||||
|
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.DisplayProp);
|
||||||
|
return prop.GetValue(x) as string ?? "N/A";
|
||||||
|
});
|
||||||
|
|
||||||
|
var searchFunc = new Func<TProp, string>(x =>
|
||||||
|
{
|
||||||
|
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.SelectorProp);
|
||||||
|
return prop.GetValue(x) as string ?? "N/A";
|
||||||
|
});
|
||||||
|
|
||||||
|
<SmartDropdown @bind-Value="Binder.Class" DisplayFunc="displayFunc" SearchProp="searchFunc" Items="Items" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var displayFunc = new Func<TProp, string>(x =>
|
||||||
|
{
|
||||||
|
var prop = typeof(TProp).GetProperties().First(x => x.Name == attribute.DisplayProp);
|
||||||
|
return prop.GetValue(x) as string ?? "N/A";
|
||||||
|
});
|
||||||
|
|
||||||
|
<SmartSelect @bind-Value="Binder.Class" DisplayField="displayFunc" Items="Items" CanBeNull="true" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter(Name = "Data")]
|
||||||
|
public object Data { get; set; }
|
||||||
|
|
||||||
|
[CascadingParameter(Name = "Property")]
|
||||||
|
public PropertyInfo Property { get; set; }
|
||||||
|
|
||||||
|
private PropBinder<TProp> Binder;
|
||||||
|
private TProp[] Items = Array.Empty<TProp>();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Binder = new(Property, Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
if (Property.GetCustomAttributes(true).Any(x => x.GetType() == typeof(SelectorAttribute)))
|
||||||
|
{
|
||||||
|
var typeToGetByDi = typeof(Repository<>).MakeGenericType(typeof(TProp));
|
||||||
|
var repo = ServiceProvider.GetRequiredService(typeToGetByDi);
|
||||||
|
var dbSet = repo.GetType().GetMethods().First(x => x.Name == "Get").Invoke(repo, null) as IEnumerable<TProp>;
|
||||||
|
Items = dbSet!.ToArray();
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
Moonlight/Shared/Components/Forms/SmartDropdown.razor
Normal file
141
Moonlight/Shared/Components/Forms/SmartDropdown.razor
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
||||||
|
@typeparam T
|
||||||
|
@inherits InputBase<T>
|
||||||
|
|
||||||
|
<div class="dropdown w-100">
|
||||||
|
<div class="input-group">
|
||||||
|
@if (CurrentValue == null)
|
||||||
|
{
|
||||||
|
<input class="form-control" type="text" @bind-value="SearchTerm" @bind-value:event="oninput" placeholder="@(Placeholder)">
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<input class="form-control" type="text" value="@(DisplayFunc(CurrentValue))">
|
||||||
|
<button class="btn btn-sm btn-primary" @onclick="() => SelectItem(default(T)!)">
|
||||||
|
<i class="bx bx-sm bx-x"></i>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var anyItems = FilteredItems.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="dropdown-menu w-100 @(anyItems ? "show" : "")" style="max-height: 200px; overflow-y: auto;">
|
||||||
|
@if (anyItems)
|
||||||
|
{
|
||||||
|
foreach (var item in FilteredItems)
|
||||||
|
{
|
||||||
|
<button class="dropdown-item py-2" type="button" @onclick="() => SelectItem(item)">@DisplayFunc(item)</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<T> Items { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<T, string> DisplayFunc { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<T, string> SearchProp { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<Task>? OnSelected { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Placeholder { get; set; } = "Search...";
|
||||||
|
|
||||||
|
private string SearchTerm
|
||||||
|
{
|
||||||
|
get => searchTerm;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
searchTerm = value;
|
||||||
|
FilteredItems = Items.OrderByDescending(x => Matches(SearchProp(x), searchTerm)).Take(30).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string searchTerm = "";
|
||||||
|
|
||||||
|
private List<T> FilteredItems = new();
|
||||||
|
|
||||||
|
private async void SelectItem(T item)
|
||||||
|
{
|
||||||
|
CurrentValue = item;
|
||||||
|
SearchTerm = "";
|
||||||
|
FilteredItems.Clear();
|
||||||
|
|
||||||
|
if (OnSelected != null)
|
||||||
|
await OnSelected.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool TryParseValueFromString(string? value, out T result, out string? validationErrorMessage)
|
||||||
|
{
|
||||||
|
// Check if the value is null or empty
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
result = default(T)!;
|
||||||
|
validationErrorMessage = "Value cannot be null or empty";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find an item that matches the search term
|
||||||
|
var item = FilteredItems.OrderByDescending(x => Matches(SearchProp(x), value)).FirstOrDefault();
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
result = item;
|
||||||
|
validationErrorMessage = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = default(T)!;
|
||||||
|
validationErrorMessage = $"No item found for search term '{value}'";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float Matches(string input, string search)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(search))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var cleanedSearch = search
|
||||||
|
.ToLower()
|
||||||
|
.Replace(" ", "")
|
||||||
|
.Replace("-", "");
|
||||||
|
|
||||||
|
var cleanedInput = input
|
||||||
|
.ToLower()
|
||||||
|
.Replace(" ", "")
|
||||||
|
.Replace("-", "");
|
||||||
|
|
||||||
|
if (cleanedInput == cleanedSearch)
|
||||||
|
return 10000;
|
||||||
|
|
||||||
|
float matches = 0;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var c in cleanedInput)
|
||||||
|
{
|
||||||
|
if (cleanedSearch.Length > i)
|
||||||
|
{
|
||||||
|
if (c == cleanedSearch[i])
|
||||||
|
matches++;
|
||||||
|
else
|
||||||
|
matches--;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches = matches / searchTerm.Length;
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
@using Moonlight.App.Models.Forms.Store
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Mappy.Net
|
||||||
|
|
||||||
|
@inject Repository<Coupon> CouponRepository
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
<SmartModal @ref="Modal" CssClasses="modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fs-3">Add new modal</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Code</label>
|
||||||
|
<input @bind="Form.Code" class="form-control" type="text"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Percent</label>
|
||||||
|
<input @bind="Form.Percent" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="form-label">Amount</label>
|
||||||
|
<input @bind="Form.Amount" class="form-control" type="number"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</SmartForm>
|
||||||
|
</SmartModal>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public Func<Task> OnUpdate { get; set; }
|
||||||
|
|
||||||
|
private SmartModal Modal;
|
||||||
|
private AddCouponForm Form = new();
|
||||||
|
|
||||||
|
public async Task Show()
|
||||||
|
{
|
||||||
|
await Modal.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnValidSubmit()
|
||||||
|
{
|
||||||
|
var coupon = Mapper.Map<Coupon>(Form);
|
||||||
|
CouponRepository.Add(coupon);
|
||||||
|
|
||||||
|
Form = new();
|
||||||
|
await ToastService.Success("Successfully added new coupon");
|
||||||
|
await Modal.Hide();
|
||||||
|
await OnUpdate.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,70 @@
|
|||||||
@page "/admin/store/coupons"
|
@page "/admin/store/coupons"
|
||||||
|
|
||||||
|
@using BlazorTable
|
||||||
|
@using Moonlight.App.Database.Entities.Store
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.Shared.Components.Modals.Store
|
||||||
|
|
||||||
|
@inject Repository<Coupon> CouponRepository
|
||||||
|
@inject Repository<CouponUse> CouponUseRepository
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
<AdminStoreNavigation Index="1" />
|
<AdminStoreNavigation Index="1" />
|
||||||
|
|
||||||
@*<div class="card card"*@
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Coupons</h3>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<button @onclick="() => AddCouponModal.Show()" class="btn btn-icon btn-success"><i class="bx bx-sm bx-plus"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||||
|
<Table TableItem="Coupon"
|
||||||
|
Items="AllCoupons"
|
||||||
|
PageSize="50"
|
||||||
|
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||||
|
TableHeadClass="fw-bold text-muted">
|
||||||
|
<Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||||
|
<Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/>
|
||||||
|
<Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/>
|
||||||
|
<Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/>
|
||||||
|
<Column TableItem="Coupon" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||||
|
<Template>
|
||||||
|
<a @onclick="() => Remove(context)" @onclick:preventDefault href="#" class="text-danger">Remove</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
</Table>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddCouponModal @ref="AddCouponModal" OnUpdate="() => LazyLoader.Reload()" />
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private Coupon[] AllCoupons;
|
||||||
|
|
||||||
|
private LazyLoader LazyLoader;
|
||||||
|
private AddCouponModal AddCouponModal;
|
||||||
|
|
||||||
|
private Task Load(LazyLoader _)
|
||||||
|
{
|
||||||
|
AllCoupons = CouponRepository
|
||||||
|
.Get()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Remove(Coupon coupon)
|
||||||
|
{
|
||||||
|
if (CouponUseRepository.Get().Any(x => x.Coupon.Id == coupon.Id))
|
||||||
|
throw new DisplayException("The coupon has been used so it cannot be deleted");
|
||||||
|
|
||||||
|
CouponRepository.Delete(coupon);
|
||||||
|
|
||||||
|
await ToastService.Success("Successfully deleted coupon");
|
||||||
|
await LazyLoader.Reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user