feat : ProductPage and CategoryPage

complete category edit and creation , complete search for category and products page
release
Amir Hossein Khademi 2024-01-28 20:51:50 +03:30
parent a8af53c9d9
commit 639d858476
30 changed files with 1344 additions and 138 deletions

View File

@ -0,0 +1,55 @@
<MudButton Variant="@Variant"
Color="@Color"
@onclick="OnClickCallback"
@attributes="CapturedAttributes"
Disabled="IsProcessing"
DisableElevation="true"
>
@if (IsProcessing)
{
<div class="flex flex-row w-full mx-auto">
<MudProgressCircular class="my-auto mr-1 -ml-4" Size="Size.Small" Indeterminate="true" />
<p class="font-extrabold my-1 mx-auto text-lg">منتظر بمانید</p>
</div>
}
else
{
<div class="flex flex-row w-full">
@if (Variant == Variant.Filled)
{
<div class="-mr-2 -ml-6 w-10 h-10 items-center bg-[#000000] bg-opacity-20 rounded-md">
<MudIcon Icon="@Icon" class="w-5 h-5 mt-2.5"></MudIcon>
</div>
}
else
{
<div class="-mr-2 -ml-6 w-12 h-12 items-center rounded-md">
<MudIcon Icon="@Icon" class="w-7 h-7 mt-2"></MudIcon>
</div>
}
<p class="my-auto mx-auto font-extrabold">@Content</p>
</div>
}
</MudButton>
@code
{
[Parameter]
public Variant Variant { get; set; }
[Parameter]
public Color Color { get; set; }
[Parameter]
public string Content { get; set; } = string.Empty;
[Parameter]
public string Icon { get; set; } = string.Empty;
[Parameter]
public bool IsProcessing { get; set; } = false;
[Parameter]
public EventCallback OnClickCallback { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> CapturedAttributes { get; set; } = new();
}

View File

@ -61,13 +61,13 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
TreeItems.Add(new TreeItemData("داشبورد", Icons.Material.Outlined.Dashboard, "HomePage")); TreeItems.Add(new TreeItemData("داشبورد", Icons.Material.Outlined.Dashboard, ""));
TreeItems.Add(new TreeItemData("فروش", Icons.Material.Outlined.ShoppingCart, "ProductsPage")); TreeItems.Add(new TreeItemData("فروش", Icons.Material.Outlined.ShoppingCart, "products"));
TreeItems.Add(new TreeItemData("محصولات", Icons.Material.Outlined.CenterFocusStrong, "ProductsPage")); TreeItems.Add(new TreeItemData("محصولات", Icons.Material.Outlined.CenterFocusStrong, "products"));
TreeItems.Add(new TreeItemData("دسته بندی ها", Icons.Material.Outlined.AllInbox, "CategoriesPage")); TreeItems.Add(new TreeItemData("دسته بندی ها", Icons.Material.Outlined.AllInbox, "categories"));
TreeItems.Add(new TreeItemData("برند ها", Icons.Custom.Brands.Facebook, "BrandsPage")); TreeItems.Add(new TreeItemData("برند ها", Icons.Custom.Brands.Facebook, "brands"));
TreeItems.Add(new TreeItemData("مشترکین", Icons.Material.Outlined.People, "ProductsPage")); TreeItems.Add(new TreeItemData("مشترکین", Icons.Material.Outlined.People, "products"));
TreeItems.Add(new TreeItemData("وبلاگ", Icons.Material.Outlined.Web, "ProductsPage")); TreeItems.Add(new TreeItemData("وبلاگ", Icons.Material.Outlined.Web, "products"));
TreeItems.Add(new TreeItemData("تنظیمات", Icons.Material.Outlined.Settings, "ProductsPage")); TreeItems.Add(new TreeItemData("تنظیمات", Icons.Material.Outlined.Settings, "products"));
} }
} }

View File

@ -1,5 +1,86 @@
<h3>BrandActionDialogBox</h3> @using NetinaShop.Common.Models.Exception
@using NetinaShop.Domain.CommandQueries.Commands
@using NetinaShop.Domain.Entities.Brands
@inject ISnackbar Snackbar
@inject IRestWrapper RestWrapper
@inject IUserUtility UserUtility
<MudDialog class="mx-auto">
<DialogContent>
<MudStack>
<MudDivider class="-mt-3" />
<MudStack Spacing="0">
<MudText Typo="Typo.h6">اطلاعات کلی</MudText>
<MudText Typo="Typo.caption">اطلاعات کلی دسته بندی محصول را به دقت وارد کنید</MudText>
</MudStack>
<MudGrid>
<MudItem lg="6" md="6">
<MudTextField T="string" Label="نام برند" @bind-Value="@_name" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem lg="6" md="6">
<MudSelect T="bool" Label="آیا صفحه شخصی دارد ؟" @bind-Value="@_isMain" ToStringFunc="b=>b.ToPersianString()" Variant="Variant.Outlined" AnchorOrigin="Origin.BottomCenter">
<MudSelectItem T="bool" Value="true"></MudSelectItem>
<MudSelectItem T="bool" Value="false" />
</MudSelect>
</MudItem>
<MudItem lg="12" md="12">
<MudTextField T="string" Label="توضیحاتــ" @bind-Value="@_description" Variant="Variant.Outlined"></MudTextField>
</MudItem>
</MudGrid>
</MudStack>
</DialogContent>
<DialogActions>
@* <MudStack>
<BaseButtonUi Icon="@Icons.Material.Outlined.Check" Variant="Variant.Filled" Color="Color.Success" Content="تایید" OnClickCallback="SubmitCreateAsync" />
<MudButton Variant="Variant.Outlined" Size="Size.Large" Color="Color.Error" OnClick="Cancel">بستن</MudButton>
</MudStack> *@
<MudStack Row="true" class="w-full mx-4 mb-2">
<BaseButtonUi class="w-64 rounded-md" IsProcessing="@_isProcessing" Icon="@Icons.Material.Outlined.Check" Variant="Variant.Filled" Color="Color.Success" Content="تایید" OnClickCallback="SubmitCreateAsync" />
<MudSpacer />
<MudButton Variant="Variant.Outlined" Size="Size.Large" Color="Color.Error" OnClick="Cancel">بستن</MudButton>
</MudStack>
</DialogActions>
</MudDialog>
@code { @code {
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
void Cancel() => MudDialog.Cancel();
private bool _isProcessing = false;
private string _name = string.Empty;
private string _description = string.Empty;
private bool _isMain;
private async Task SubmitCreateAsync()
{
try
{
if (_name.IsNullOrEmpty())
throw new AppException("لطفا نام برند را وارد کنید");
_isProcessing = true;
var token = await UserUtility.GetBearerTokenAsync();
var request = new CreateBrandCommand(_name, _description, _isMain, string.Empty, new List<StorageFileSDto>());
await RestWrapper.CrudApiRest<Brand, Guid>(Address.BrandController).Create<CreateBrandCommand>(request, token);
MudDialog.Close(DialogResult.Ok(true));
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
if (exe != null)
Snackbar.Add(exe.Message, Severity.Error);
Snackbar.Add(ex.Content, Severity.Error);
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
}
finally
{
_isProcessing = false;
}
}
} }

View File

@ -37,21 +37,28 @@
<MudText Typo="Typo.caption">می توانید توضیحاتــ تکمیلی محصول را کامل وارد کنید</MudText> <MudText Typo="Typo.caption">می توانید توضیحاتــ تکمیلی محصول را کامل وارد کنید</MudText>
</MudStack> </MudStack>
<MudItem lg="12" md="12"> <MudItem lg="12" md="12">
<RadzenHtmlEditor> <RadzenHtmlEditor class="min-h-[10rem]">
<RadzenHtmlEditorUndo/> <RadzenHtmlEditorUndo />
<RadzenHtmlEditorRedo/> <RadzenHtmlEditorRedo />
<RadzenHtmlEditorSeparator/> <RadzenHtmlEditorSeparator />
<RadzenHtmlEditorAlignLeft/>
<RadzenHtmlEditorAlignCenter/> <RadzenHtmlEditorFormatBlock />
<RadzenHtmlEditorAlignRight/> <RadzenHtmlEditorSeparator />
<RadzenHtmlEditorJustify/>
<RadzenHtmlEditorSeparator/> <RadzenHtmlEditorAlignLeft />
<RadzenHtmlEditorBold/> <RadzenHtmlEditorAlignCenter />
<RadzenHtmlEditorItalic/> <RadzenHtmlEditorAlignRight />
<RadzenHtmlEditorUnderline/> <RadzenHtmlEditorJustify />
<RadzenHtmlEditorStrikeThrough/> <RadzenHtmlEditorSeparator />
<RadzenHtmlEditorSeparator/>
<RadzenHtmlEditorRemoveFormat/> <RadzenHtmlEditorSource />
<RadzenHtmlEditorBold />
<RadzenHtmlEditorItalic />
<RadzenHtmlEditorUnderline />
<RadzenHtmlEditorStrikeThrough />
<RadzenHtmlEditorRemoveFormat />
<RadzenHtmlEditorSeparator />
</RadzenHtmlEditor> </RadzenHtmlEditor>
</MudItem> </MudItem>
@ -77,4 +84,22 @@
void Submit() => MudDialog.Close(DialogResult.Ok(true)); void Submit() => MudDialog.Close(DialogResult.Ok(true));
void Cancel() => MudDialog.Cancel(); void Cancel() => MudDialog.Cancel();
private bool _isEditing = false;
private bool _isProcessing = false;
private ProductSDto? _product = null;
[Parameter]
public ProductSDto? Product
{
get => _product;
set
{
_product = value;
if (_product != null)
{
_isEditing = true;
}
}
}
} }

View File

@ -1,10 +1,13 @@
@using NetinaShop.Common.Models.Exception @using NetinaShop.Common.Models.Exception
@using NetinaShop.Domain.CommandQueries.Commands @using NetinaShop.Domain.CommandQueries.Commands
@using Radzen.Blazor
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IRestWrapper RestWrapper @inject IRestWrapper RestWrapper
@inject IUserUtility UserUtility @inject IUserUtility UserUtility
<head>
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
</head>
<MudDialog class="mx-auto"> <MudDialog class="mx-auto">
<DialogContent> <DialogContent>
<MudStack> <MudStack>
@ -16,10 +19,10 @@
</MudStack> </MudStack>
<MudGrid> <MudGrid>
<MudItem lg="4" md="6"> <MudItem lg="4" md="6">
<MudTextField T="string" Label="نام دسته بندی" Variant="Variant.Outlined"></MudTextField> <MudTextField T="string" @bind-Value="@_name" Label="نام دسته بندی" Variant="Variant.Outlined"></MudTextField>
</MudItem> </MudItem>
<MudItem lg="4" md="6"> <MudItem lg="4" md="6">
<MudSelect T="bool" Label="آیا دسته بندی اصلی است ؟" ToStringFunc="b=>b.ToPersianString()" Variant="Variant.Outlined" AnchorOrigin="Origin.BottomCenter"> <MudSelect T="bool" @bind-Value="@_isMain" Label="آیا دسته بندی اصلی است ؟" ToStringFunc="b=>b.ToPersianString()" Variant="Variant.Outlined" AnchorOrigin="Origin.BottomCenter">
<MudSelectItem T="bool" Value="true" ></MudSelectItem> <MudSelectItem T="bool" Value="true" ></MudSelectItem>
<MudSelectItem T="bool" Value="false" /> <MudSelectItem T="bool" Value="false" />
</MudSelect> </MudSelect>
@ -46,29 +49,91 @@
</MudAutocomplete> </MudAutocomplete>
</MudItem> </MudItem>
<MudItem lg="12" md="12"> <MudItem lg="12" md="12">
<MudTextField T="string" Label="توضیحاتــ" Variant="Variant.Outlined"></MudTextField> <MudStack class="mt-4 mb-2" Spacing="0">
<MudText Typo="Typo.h6">توضیحات تکمیلی</MudText>
<MudText Typo="Typo.caption">می توانید توضیحاتــ تکمیلی محصول را کامل وارد کنید</MudText>
</MudStack>
<RadzenHtmlEditor @bind-Value="@_description" class="min-h-[10rem]">
<RadzenHtmlEditorUndo/>
<RadzenHtmlEditorRedo/>
<RadzenHtmlEditorSeparator />
<RadzenHtmlEditorFormatBlock/>
<RadzenHtmlEditorSeparator />
<RadzenHtmlEditorAlignLeft/>
<RadzenHtmlEditorAlignCenter/>
<RadzenHtmlEditorAlignRight/>
<RadzenHtmlEditorJustify/>
<RadzenHtmlEditorSeparator/>
<RadzenHtmlEditorSource />
<RadzenHtmlEditorBold/>
<RadzenHtmlEditorItalic/>
<RadzenHtmlEditorUnderline/>
<RadzenHtmlEditorStrikeThrough />
<RadzenHtmlEditorRemoveFormat />
<RadzenHtmlEditorSeparator/>
</RadzenHtmlEditor>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
</MudStack> </MudStack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudStack class="w-full mx-4" Row="true">
<MudButton DisableElevation="true" Size="Size.Large" StartIcon="@Icons.Material.Outlined.Check" Variant="Variant.Filled" Color="Color.Success" OnClick="Submit">تایید</MudButton>
<MudButton Variant="Variant.Outlined" Size="Size.Large" Color="Color.Error" OnClick="Cancel">بستن</MudButton>
<MudStack Row="true" class="w-full mx-4 mb-2">
@if (_isEditing)
{
<BaseButtonUi class="w-64 rounded-md" IsProcessing="@_isProcessing"
Icon="@Icons.Material.Outlined.Check"
Variant="Variant.Filled" Color="Color.Success"
Content="ثبت ویرایش" OnClickCallback="SubmitEditAsync" />
}
else
{
<BaseButtonUi class="w-64 rounded-md" IsProcessing="@_isProcessing"
Icon="@Icons.Material.Outlined.Check"
Variant="Variant.Filled" Color="Color.Success"
Content="تایید" OnClickCallback="SubmitCreateAsync" />
}
<MudSpacer />
<MudButton Variant="Variant.Outlined" Size="Size.Large" Color="Color.Error" OnClick="Cancel">بستن</MudButton>
</MudStack> </MudStack>
</DialogActions> </DialogActions>
</MudDialog> </MudDialog>
@code { @code
[CascadingParameter] MudDialogInstance MudDialog { get; set; } {
[CascadingParameter]
MudDialogInstance MudDialog { get; set; }
void Submit() => MudDialog.Close(DialogResult.Ok(true));
void Cancel() => MudDialog.Cancel(); void Cancel() => MudDialog.Cancel();
private string _name; private string _name = string.Empty;
private string _description; private string _description = string.Empty;
private bool _isMain; private bool _isMain;
private bool _isEditing = false;
private bool _isProcessing = false;
private ProductCategorySDto? _category = null;
[Parameter]
public ProductCategorySDto? Category
{
get => _category;
set
{
_category = value;
if (_category != null)
{
_name = _category.Name;
_description = _category.Description;
_isEditing = true;
}
}
}
private async Task SubmitCreateAsync() private async Task SubmitCreateAsync()
{ {
@ -76,9 +141,11 @@
{ {
if (_name.IsNullOrEmpty()) if (_name.IsNullOrEmpty())
throw new AppException("لطفا نام دسته را وارد کنید"); throw new AppException("لطفا نام دسته را وارد کنید");
_isProcessing = true;
var token = await UserUtility.GetBearerTokenAsync(); var token = await UserUtility.GetBearerTokenAsync();
var request = new ProductCreateCategoryCommand(_name, _description, _selectedCategory?.Id ?? default, new List<StorageFileSDto>()); var request = new CreateProductCategoryCommand(_name, _description,_isMain, _selectedCategory?.Id ?? default, new List<StorageFileSDto>());
await RestWrapper.CrudApiRest<ProductCategory, Guid>(Address.ProductCategoryController).Create<ProductCreateCategoryCommand>(request, token); await RestWrapper.CrudApiRest<ProductCategory, Guid>(Address.ProductCategoryController).Create<CreateProductCategoryCommand>(request, token);
MudDialog.Close();
} }
catch (ApiException ex) catch (ApiException ex)
{ {
@ -86,10 +153,52 @@
if (exe != null) if (exe != null)
Snackbar.Add(exe.Message, Severity.Error); Snackbar.Add(exe.Message, Severity.Error);
Snackbar.Add(ex.Content, Severity.Error); Snackbar.Add(ex.Content, Severity.Error);
MudDialog.Cancel();
} }
catch (Exception e) catch (Exception e)
{ {
Snackbar.Add(e.Message, Severity.Error); Snackbar.Add(e.Message, Severity.Error);
MudDialog.Cancel();
}
finally
{
_isProcessing = false;
}
}
private async Task SubmitEditAsync()
{
try
{
if (Category == null)
throw new AppException("دسته به درستی ارسال نشده است");
if (_name.IsNullOrEmpty())
throw new AppException("لطفا نام دسته را وارد کنید");
_isProcessing = true;
await Task.Delay(1000);
var token = await UserUtility.GetBearerTokenAsync();
var request = new UpdateProductCategoryCommand(Category.Id, _name, _description,_isMain, _selectedCategory?.Id ?? default, new List<StorageFileSDto>());
await RestWrapper.CrudApiRest<ProductCategory, Guid>(Address.ProductCategoryController).Update<UpdateProductCategoryCommand>(request, token);
MudDialog.Close();
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
if (exe != null)
Snackbar.Add(exe.Message, Severity.Error);
Snackbar.Add(ex.Content, Severity.Error);
MudDialog.Cancel();
}
catch (Exception e)
{
Snackbar.Add(e.Message, Severity.Error);
MudDialog.Cancel();
}
finally
{
_isProcessing = false;
} }
} }
@ -101,7 +210,7 @@
{ {
if (_productCategories.Count == 0) if (_productCategories.Count == 0)
{ {
_productCategories = await RestWrapper.CrudDtoApiRest<ProductCategory, ProductCategorySDto, Guid>(Address.ProductCategoryController).ReadAll(); _productCategories = await RestWrapper.ProductCategoryRestApi.ReadAll();
} }
if (city.IsNullOrEmpty()) if (city.IsNullOrEmpty())
return _productCategories; return _productCategories;

View File

@ -1,5 +1,9 @@
@inherits LayoutComponentBase @using NetinaShop.AdminPanel.PWA.Pages
@using Radzen.Blazor
@inherits LayoutComponentBase
@inject IPWAUpdaterService PwaUpdaterService @inject IPWAUpdaterService PwaUpdaterService
@inject NavigationManager NavigationManager
@inject IUserUtility UserUtility
<style> <style>
body .pwa-updater[b-pwa-updater] { body .pwa-updater[b-pwa-updater] {
--pwa-updater-bar-height: 40px; --pwa-updater-bar-height: 40px;
@ -45,7 +49,7 @@
</MudStack> </MudStack>
<MudSpacer /> <MudSpacer />
<MudIconButton Size="Size.Medium" Color="Color.Inherit" Icon="@Icons.Material.Outlined.Settings"/> <MudIconButton Size="Size.Medium" Color="Color.Inherit" Icon="@Icons.Material.Outlined.Settings"/>
<MudIconButton Size="Size.Medium" Color="Color.Error" Icon="@Icons.Material.Outlined.ExitToApp" /> <MudIconButton Size="Size.Medium" Color="Color.Error" OnClick="LogoutAsync" Icon="@Icons.Material.Outlined.ExitToApp" />
</MudAppBar> </MudAppBar>
<MudGrid Spacing="0"> <MudGrid Spacing="0">
@ -77,7 +81,7 @@
<MudLayout> <MudLayout>
<div> <div>
@Body <LoginPage/>
<div dir="ltr"> <div dir="ltr">
<PWAUpdater Text="@_updateText" ButtonCaption="اپدیت کنید"/> <PWAUpdater Text="@_updateText" ButtonCaption="اپدیت کنید"/>
</div> </div>
@ -111,6 +115,12 @@
}; };
private string _updateText = "! نسخه جدید پنل ادمین رسید"; private string _updateText = "! نسخه جدید پنل ادمین رسید";
private async Task LogoutAsync()
{
await UserUtility.LogoutAsync();
NavigationManager.NavigateTo("login",true,true);
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
string? version = typeof(Program)?.Assembly.GetName()?.Version?.ToString(); string? version = typeof(Program)?.Assembly.GetName()?.Version?.ToString();

View File

@ -3,13 +3,14 @@
public static class Address public static class Address
{ {
#if DEBUG #if DEBUG
//public static string BaseAddress = "http://localhost:32770/api"; public static string BaseAddress = "http://localhost:32770/api";
public static string BaseAddress = "https://apinetinashop.visabartar.com/api"; //public static string BaseAddress = "https://apinetinashop.visabartar.com/api";
#else #else
public static string BaseAddress = "https://apinetinashop.visabartar.com/api"; public static string BaseAddress = "https://apinetinashop.visabartar.com/api";
#endif #endif
public static string AuthController = $"{BaseAddress}/auth"; public static string AuthController = $"{BaseAddress}/auth";
public static string UserController = $"{BaseAddress}/user"; public static string UserController = $"{BaseAddress}/user";
public static string ProductCategoryController = $"{BaseAddress}/product/category"; public static string ProductCategoryController = $"{BaseAddress}/product/category";
public static string ProductController = $"{BaseAddress}/product";
public static string BrandController = $"{BaseAddress}/brand"; public static string BrandController = $"{BaseAddress}/brand";
} }

View File

@ -37,12 +37,12 @@
<ItemGroup> <ItemGroup>
<Folder Include="Utilities\" /> <Folder Include="Utilities\" />
<Folder Include="Services\RestServices\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="Blazored.LocalStorage" /> <Using Include="Blazored.LocalStorage" />
<Using Include="Microsoft.AspNetCore.Components" /> <Using Include="Microsoft.AspNetCore.Components" />
<Using Include="Microsoft.IdentityModel.Tokens" />
<Using Include="Microsoft.JSInterop" /> <Using Include="Microsoft.JSInterop" />
<Using Include="MudBlazor" /> <Using Include="MudBlazor" />
<Using Include="NetinaShop.AdminPanel.PWA.Dialogs" /> <Using Include="NetinaShop.AdminPanel.PWA.Dialogs" />
@ -52,12 +52,15 @@
<Using Include="NetinaShop.AdminPanel.PWA.Services.RestServices" /> <Using Include="NetinaShop.AdminPanel.PWA.Services.RestServices" />
<Using Include="NetinaShop.AdminPanel.PWA.Utilities" /> <Using Include="NetinaShop.AdminPanel.PWA.Utilities" />
<Using Include="NetinaShop.Common.Models.Api" /> <Using Include="NetinaShop.Common.Models.Api" />
<Using Include="NetinaShop.Common.Models.Exception" />
<Using Include="NetinaShop.Domain.Dtos.RequestDtos" /> <Using Include="NetinaShop.Domain.Dtos.RequestDtos" />
<Using Include="NetinaShop.Domain.Dtos.SmallDtos" /> <Using Include="NetinaShop.Domain.Dtos.SmallDtos" />
<Using Include="NetinaShop.Domain.Entities.ProductCategories" /> <Using Include="NetinaShop.Domain.Entities.ProductCategories" />
<Using Include="NetinaShop.Domain.Entities.Products" />
<Using Include="NetinaShop.Domain.Enums" /> <Using Include="NetinaShop.Domain.Enums" />
<Using Include="Newtonsoft.Json" /> <Using Include="Newtonsoft.Json" />
<Using Include="Refit" /> <Using Include="Refit" />
<Using Include="System.Collections.ObjectModel" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,4 @@
@page "/BrandsPage" @page "/brands"
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager

View File

@ -69,6 +69,7 @@ public class BrandsPageViewModel : BaseViewModel<List<BrandSDto>>
await _restWrapper.CrudDtoApiRest<Brand, BrandSDto, Guid>(Address.BrandController) await _restWrapper.CrudDtoApiRest<Brand, BrandSDto, Guid>(Address.BrandController)
.Delete(selectedCategoryId, token); .Delete(selectedCategoryId, token);
_snackbar.Add("حذف برند با موفقیت انجام شد", Severity.Success); _snackbar.Add("حذف برند با موفقیت انجام شد", Severity.Success);
await InitializeAsync();
} }
catch (ApiException ex) catch (ApiException ex)

View File

@ -1,4 +1,5 @@
@page "/CategoriesPage" @using Radzen.Blazor
@page "/categories"
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IRestWrapper RestWrapper @inject IRestWrapper RestWrapper
@ -13,25 +14,35 @@
<MudText Typo="Typo.h4">دسته بندی ها</MudText> <MudText Typo="Typo.h4">دسته بندی ها</MudText>
<MudChip Color="Color.Info" Variant="Variant.Outlined">124 عدد</MudChip> <MudChip Color="Color.Info" Variant="Variant.Outlined">124 عدد</MudChip>
<MudSpacer /> <MudSpacer/>
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled"
DisableElevation="true" DisableElevation="true"
StartIcon="@Icons.Material.Outlined.Add" StartIcon="@Icons.Material.Outlined.Add"
Color="Color.Secondary" Color="Color.Secondary"
OnClick="ViewModel.AddProductClicked" OnClick="ViewModel.AddProductCategoryClicked"
class="my-auto">افزودن دسته بندی</MudButton> class="my-auto">افزودن دسته بندی</MudButton>
</MudStack> </MudStack>
<MudPaper> <MudPaper>
<MudDataGrid Striped="true" T="ProductCategorySDto" Items="@ViewModel.PageDto" Filterable="false" Loading="@ViewModel.IsProcessing" <MudDataGrid FixedFooter="true" FixedHeader="true" Striped="true"
T="ProductCategorySDto" Items="@ViewModel.PageDto" CurrentPage="@ViewModel.CurrentPage"
RowsPerPage="15" Filterable="false" Loading="@ViewModel.IsProcessing"
SortMode="@SortMode.None" Groupable="false"> SortMode="@SortMode.None" Groupable="false">
<ToolBarContent> <ToolBarContent>
<MudTextField T="string" Placeholder="جست جو بر اساس نام" Adornment="Adornment.Start" Immediate="true" <MudTextField T="string" Placeholder="جست جو بر اساس نام" Adornment="Adornment.Start" Immediate="true"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" class="my-auto"></MudTextField> AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" class="my-auto"
@bind-Value="@ViewModel.Search"
OnAdornmentClick="@ViewModel.SearchAsync"></MudTextField>
</ToolBarContent> </ToolBarContent>
<Columns> <Columns>
<PropertyColumn Title="نام دسته" Property="arg => arg.Name"/> <HierarchyColumn T="ProductCategorySDto" ButtonDisabledFunc="@(x => x.Description.IsNullOrEmpty())" />
<PropertyColumn Title="توضیحاتــ" Property="arg => arg.Description"/> <PropertyColumn Title="نام دسته" Resizable="false" Property="arg => arg.Name"/>
<TemplateColumn Title="دسته اصلی" T="ProductCategorySDto"> @* <TemplateColumn Title="توضیحاتــ" Resizable="false" T="ProductCategorySDto">
<CellTemplate>
<p class="line-clamp-1">@context.Item.Description</p>
</CellTemplate>
</TemplateColumn> *@
<TemplateColumn Resizable="false" Title="دسته اصلی" T="ProductCategorySDto">
<CellTemplate> <CellTemplate>
@if (@context.Item.IsMain) @if (@context.Item.IsMain)
{ {
@ -44,19 +55,46 @@
} }
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<PropertyColumn Title="دسته پدر" Resizable="false" Property="arg => arg.ParentName" />
<TemplateColumn CellClass="d-flex justify-end"> <TemplateColumn CellClass="d-flex justify-end">
<CellTemplate> <CellTemplate>
<MudStack Row="true"> <MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="@Size.Small" Variant="@Variant.Outlined" Color="@Color.Info"></MudIconButton> <MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="@Size.Small"
Variant="@Variant.Outlined"
OnClick="async () => await ViewModel.EditProductCategoryClicked(context.Item)"
Color="@Color.Info"/>
<MudIconButton Icon="@Icons.Material.Filled.Delete" <MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="@Size.Small" Size="@Size.Small"
Variant="@Variant.Outlined" Variant="@Variant.Outlined"
OnClick="async() => await ViewModel.DeleteProductCategoryAsync(context.Item.Id)" OnClick="async () => await ViewModel.DeleteProductCategoryAsync(context.Item.Id)"
Color="@Color.Error"></MudIconButton> Color="@Color.Error"/>
</MudStack> </MudStack>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
</Columns> </Columns>
<ChildRowContent>
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">توضیحاتــ</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText>@context.Item.Description</MudText>
</MudCardContent>
</MudCard>
</ChildRowContent>
<PagerContent>
<MudStack Row="true" class="w-full">
<MudPagination Rectangular="true" Variant="Variant.Filled" Count="@ViewModel.PageCount"
SelectedChanged="@ViewModel.ChangePageAsync" class="my-4 mx-auto"/>
</MudStack>
</PagerContent>
</MudDataGrid> </MudDataGrid>
</MudPaper> </MudPaper>
</MudItem> </MudItem>

View File

@ -1,6 +1,9 @@
namespace NetinaShop.AdminPanel.PWA.Pages; using System.Collections.ObjectModel;
using NetinaShop.Common.Models.Exception;
public class CategoriesPageViewModel : BaseViewModel<List<ProductCategorySDto>> namespace NetinaShop.AdminPanel.PWA.Pages;
public class CategoriesPageViewModel : BaseViewModel<ObservableCollection<ProductCategorySDto>>
{ {
private readonly NavigationManager _navigationManager; private readonly NavigationManager _navigationManager;
private readonly ISnackbar _snackbar; private readonly ISnackbar _snackbar;
@ -8,6 +11,10 @@ public class CategoriesPageViewModel : BaseViewModel<List<ProductCategorySDto>>
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IRestWrapper _restWrapper; private readonly IRestWrapper _restWrapper;
public string Search = string.Empty;
public int CurrentPage = 0;
public int PageCount = 1;
public CategoriesPageViewModel(NavigationManager navigationManager, ISnackbar snackbar, IUserUtility userUtility, IRestWrapper restWrapper, IDialogService dialogService) public CategoriesPageViewModel(NavigationManager navigationManager, ISnackbar snackbar, IUserUtility userUtility, IRestWrapper restWrapper, IDialogService dialogService)
{ {
_navigationManager = navigationManager; _navigationManager = navigationManager;
@ -22,9 +29,12 @@ public class CategoriesPageViewModel : BaseViewModel<List<ProductCategorySDto>>
try try
{ {
IsProcessing = true; IsProcessing = true;
PageDto.Clear();
var dto = await _restWrapper.CrudDtoApiRest<ProductCategory, ProductCategorySDto, Guid>(Address.ProductCategoryController) var dto = await _restWrapper.CrudDtoApiRest<ProductCategory, ProductCategorySDto, Guid>(Address.ProductCategoryController)
.ReadAll(); .ReadAll(CurrentPage);
PageDto = dto; dto.ForEach(d=>PageDto.Add(d));
if (PageDto.Count == 15)
PageCount = 2;
} }
catch (ApiException ex) catch (ApiException ex)
{ {
@ -43,12 +53,53 @@ public class CategoriesPageViewModel : BaseViewModel<List<ProductCategorySDto>>
await base.InitializeAsync(); await base.InitializeAsync();
} }
public async Task AddProductClicked() public async Task ChangePageAsync(int page)
{
CurrentPage = page-1;
if (CurrentPage > PageCount-2)
{
try
{
IsProcessing = true;
var dto = await _restWrapper.CrudDtoApiRest<ProductCategory, ProductCategorySDto, Guid>(Address.ProductCategoryController)
.ReadAll(CurrentPage);
dto.ForEach(d => PageDto.Add(d));
if(PageDto.Count % 15==0)
PageCount = CurrentPage + 2;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}
public async Task AddProductCategoryClicked()
{ {
DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true }; DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true };
await _dialogService.ShowAsync<ProductCategoryActionDialogBox>("افزودن دسته جدید", maxWidth); await _dialogService.ShowAsync<ProductCategoryActionDialogBox>("افزودن دسته جدید", maxWidth);
} }
public async Task EditProductCategoryClicked(ProductCategorySDto category)
{
DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true };
var parameters = new DialogParameters<ProductCategoryActionDialogBox>();
parameters.Add(x=>x.Category,category);
await _dialogService.ShowAsync<ProductCategoryActionDialogBox>($"ویرایش دسته {category.Name}", parameters, maxWidth);
}
public async Task DeleteProductCategoryAsync(Guid selectedCategoryId) public async Task DeleteProductCategoryAsync(Guid selectedCategoryId)
{ {
var options = new DialogOptions { CloseOnEscapeKey = true }; var options = new DialogOptions { CloseOnEscapeKey = true };
@ -85,4 +136,35 @@ public class CategoriesPageViewModel : BaseViewModel<List<ProductCategorySDto>>
} }
} }
} }
public async Task SearchAsync()
{
try
{
if (Search.IsNullOrEmpty())
throw new AppException("دسته بندی برای جست جو وارد نشده است");
IsProcessing = true;
CurrentPage = 0;
PageCount = 1;
PageDto.Clear();
var dto = await _restWrapper.ProductCategoryRestApi.ReadAll(Search);
dto.ForEach(d => PageDto.Add(d));
if (PageDto.Count == 15)
PageCount = 2;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
} }

View File

@ -1,5 +1,5 @@
 @page "/"
@page "/" @page "/home"
@attribute [Microsoft.AspNetCore.Authorization.Authorize] @attribute [Microsoft.AspNetCore.Authorization.Authorize]
<MudStack class="w-screen h-screen bg-[--color-background]"> <MudStack class="w-screen h-screen bg-[--color-background]">

View File

@ -1,6 +1,90 @@
@page "/LoginPage" @using Microsoft.AspNetCore.Authorization
<h3>LoginPage</h3> @attribute [AllowAnonymous]
@page "/login"
@inject NavigationManager NavigationManager
@inject IRestWrapper RestWrapper
@inject ISnackbar Snackbar
@inject IUserUtility UserUtility
@* <style>
.mud-input-label {
background-color: #356859;
color: #eee !important;
}
.secondary-border > .mud-input-control-input-container > .mud-input.mud-input-outlined > input:focus ~ .mud-input-outlined-border {
border-color: rgba(253, 85, 35, 1);
color: white !important;
}
inpute {
color: white !important
}
</style> *@
<div class="w-screen h-screen bg-[#EEEEEE] p-0 flex flex-col-reverse md:flex-row overflow-hidden">
<div class="bg-white h-full basis-full md:basis-1/2 md:rounded-tr-none md:rounded-bl-xl md:rounded-tl-xl flex md:px-10 lg:px-20 py-8 px-4">
<MudStack class="my-auto mx-auto">
<div>
<MudText Typo="Typo.h5" class="text-lg text-center"><b>ورود با شماره تلفن همراه</b></MudText>
<MudText Typo="Typo.caption" Align="Align.Center">برای ورود به پنل ادمین شماره تلفن همراه خود را وارد کنید ، میتوانید با رمز عبور نیز وارد شوید</MudText>
</div>
@*<p class="text-lg sm:mt-6 mb-3 font-extrabold">شماره تلفن شما</p>
<input class="bg-transparent text-[1.8rem] sm:py-3 font-extrabold text-center border rounded-md shadow-sm focus:border-teal-500 focus:ring-teal-500"
type="text"
maxlength="11"
pattern="[0-9]"
inputmode="numeric"
autocomplete="one-time-code" required> *@
@if (@ViewModel.PhoneNumberConfirmed)
{
<MudTextField T="string" @bind-value="@ViewModel.VerifyCode" Label="کد ارسال شده" Variant="Variant.Outlined" />
}
else
{
<MudTextField T="string" @bind-Value="@ViewModel.PhoneNumber" Label="شماره تلفن همراه" Variant="Variant.Outlined" />
}
<BaseButtonUi Variant="Variant.Filled"
Icon="@Icons.Material.Outlined.Login"
OnClickCallback="@ViewModel.SubmitAsync"
Color="Color.Info"
IsProcessing="@ViewModel.IsProcessing"
class="text-xl py-3"
Content="تایید و ادامه"></BaseButtonUi>
<p class="mx-3 text-justify">
با تایید شماره تلفن همراه با همه شرایط <a class="text-blue-500">
حریم
خصوصی
</a> اپلیکیشن داکیومد موافقت می کنم
</p>
</MudStack>
</div>
<div class="h-full basis-full md:basis-1/2 md:rounded-tr-none md:rounded-bl-xl md:rounded-tl-xl flex md:px-10 lg:px-20 py-8 px-4">
<div class="mx-auto my-auto">
<dotlottie-player src="https://lottie.host/235bcb2d-0f2c-4f06-9c36-7e6677b82e00/9tGECPkU5J.json"
background="transparent" speed="1" class="mx-auto w-64 h-64 lg:w-96 lg:h-96" loop autoplay />
<MudText Typo="Typo.h5" Align="Align.Center"><b>داشبورد تخصصی نتینا</b></MudText>
<MudText Align="Align.Center">شما میتوانید با ورود به داشبورد تخصصی نتینا فروشگاه خود را مدیریت کنید </MudText>
</div>
</div>
</div>
@code { @code {
public LoginPageViewModel ViewModel { get; set; }
protected override async Task OnInitializedAsync()
{
ViewModel = new LoginPageViewModel(Snackbar,RestWrapper,NavigationManager,UserUtility);
await ViewModel.InitializeAsync();
await base.OnInitializedAsync();
}
} }

View File

@ -0,0 +1,53 @@
namespace NetinaShop.AdminPanel.PWA.Pages;
public class LoginPageViewModel : BaseViewModel
{
private readonly IRestWrapper _restWrapper;
private readonly NavigationManager _navigationManager;
private readonly IUserUtility _userUtility;
private readonly ISnackbar _snackbar;
public LoginPageViewModel(ISnackbar snackbar, IRestWrapper restWrapper,NavigationManager navigationManager,IUserUtility userUtility)
{
_snackbar = snackbar;
_restWrapper = restWrapper;
_navigationManager = navigationManager;
_userUtility = userUtility;
}
public bool PhoneNumberConfirmed { get; set; }
public string PhoneNumber { get; set; } = string.Empty;
public string VerifyCode { get; set; } = string.Empty;
public async Task SubmitAsync()
{
try
{
IsProcessing = true;
if (!PhoneNumberConfirmed)
{
await _restWrapper.AuthRestApi.GetVerifyCodeAsync(PhoneNumber);
PhoneNumberConfirmed = true;
}
else
{
var dto = await _restWrapper.AuthRestApi.LoginWithVerifyCodeAsync(new LoginRequestDto
{ UserName = PhoneNumber, VerifyCode = VerifyCode });
await _userUtility.SetUserAsync(dto.User);
await _userUtility.SetBearerTokenAsync(dto.BearerToken);
_navigationManager.NavigateTo("home",true,true);
}
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}

View File

@ -1,7 +1,11 @@
@page "/ProductsPage" @page "/products"
@attribute [Microsoft.AspNetCore.Authorization.Authorize] @attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject IDialogService DialogService @inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject ISnackbar Snackbar
@inject IUserUtility UserUtility
@inject IRestWrapper RestWrapper
<MudStack class="w-full p-8 h-screen bg-[--color-background]"> <MudStack class="w-full p-8 h-screen bg-[--color-background]">
<MudGrid> <MudGrid>
@ -14,25 +18,52 @@
DisableElevation="true" DisableElevation="true"
StartIcon="@Icons.Material.Outlined.Add" StartIcon="@Icons.Material.Outlined.Add"
Color="Color.Secondary" Color="Color.Secondary"
OnClick="AddProductClicked" OnClick="@ViewModel.AddProductClicked"
class="my-auto">افزودن محصول</MudButton> class="my-auto">افزودن محصول</MudButton>
</MudStack> </MudStack>
<MudPaper> <MudPaper>
<MudDataGrid T="ProductSDto" Items="@Products" Filterable="false" SortMode="@SortMode.None" Groupable="false"> <MudDataGrid FixedFooter="true" FixedHeader="true" Striped="true"
T="ProductSDto" Items="@ViewModel.PageDto" CurrentPage="@ViewModel.CurrentPage"
RowsPerPage="20" Filterable="false" Loading="@ViewModel.IsProcessing"
SortMode="@SortMode.None" Groupable="false">
<ToolBarContent>
<MudTextField T="string" Placeholder="جست جو بر اساس نام" Adornment="Adornment.Start" Immediate="true"
Clearable="true"
ValueChanged="@ViewModel.SearchChanged"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" class="my-auto"
OnAdornmentClick="@ViewModel.SearchAsync"></MudTextField>
</ToolBarContent>
<Columns> <Columns>
<PropertyColumn Title="نام محصول" Property="arg => arg.PersianName" /> <PropertyColumn Title="نام محصول" Property="arg => arg.PersianName"/>
<PropertyColumn Title="دسته بندی" Property="arg => arg.CategoryName" /> <PropertyColumn Title="دسته بندی" Property="arg => arg.CategoryName"/>
<PropertyColumn Title="برند" Property="arg => arg.BrandNames" /> <PropertyColumn Title="برند" Property="arg => arg.BrandNames"/>
<PropertyColumn Title="قیمتــ" Property="arg => arg.Cost" /> <PropertyColumn Title="قیمتــ" Property="arg => arg.Cost"/>
<TemplateColumn CellClass="d-flex justify-end"> <TemplateColumn CellClass="d-flex justify-end">
<CellTemplate> <CellTemplate>
<MudStack Row="true"> <MudStack Row="true">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="@Size.Small" Variant="@Variant.Outlined" Color="@Color.Info"></MudIconButton> <MudIconButton Icon="@Icons.Material.Filled.Edit"
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="@Size.Small" Variant="@Variant.Outlined" Color="@Color.Error"></MudIconButton> Size="@Size.Small"
Variant="@Variant.Outlined"
Color="@Color.Info"></MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="@Size.Small"
Variant="@Variant.Outlined"
OnClick="async () => await ViewModel.DeleteProductAsync(context.Item.Id)"
Color="@Color.Error"></MudIconButton>
</MudStack> </MudStack>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
</Columns> </Columns>
<PagerContent>
<MudStack Row="true" class="w-full">
<MudPagination Rectangular="true" Variant="Variant.Filled" Count="@ViewModel.PageCount"
SelectedChanged="@ViewModel.ChangePageAsync" class="my-4 mx-auto"/>
</MudStack>
</PagerContent>
</MudDataGrid> </MudDataGrid>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
@ -41,56 +72,11 @@
@code @code
{ {
List<ProductSDto> Products { get; set; } = new List<ProductSDto>(); public ProductsPageViewModel ViewModel { get; set; }
protected override Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Products.Add(new ProductSDto ViewModel = new ProductsPageViewModel(NavigationManager, Snackbar, UserUtility, RestWrapper, DialogService);
{ await ViewModel.InitializeAsync();
PersianName = "شامپو ضدشوره هد اند شولدرز (Head & Shoulders) مدل Lemon 2in1 arada حجم 350 میلی لیتر", await base.OnInitializedAsync();
BrandNames = "شوریدر",
CategoryName = "شوینده سر",
Cost = 1200000,
});
Products.Add(new ProductSDto
{
PersianName = "شامپو ضدشوره هد اند شولدرز (Head & Shoulders) مدل Lemon 2in1 arada حجم 350 میلی لیتر",
BrandNames = "شوریدر",
CategoryName = "شوینده سر",
Cost = 1200000,
});
Products.Add(new ProductSDto
{
PersianName = "شامپو ضدشوره هد اند شولدرز (Head & Shoulders) مدل Lemon 2in1 arada حجم 350 میلی لیتر",
BrandNames = "شوریدر",
CategoryName = "شوینده سر",
Cost = 1200000,
});
Products.Add(new ProductSDto
{
PersianName = "شامپو ضدشوره هد اند شولدرز (Head & Shoulders) مدل Lemon 2in1 arada حجم 350 میلی لیتر",
BrandNames = "شوریدر",
CategoryName = "شوینده سر",
Cost = 1200000,
});
Products.Add(new ProductSDto
{
PersianName = "شامپو ضدشوره هد اند شولدرز (Head & Shoulders) مدل Lemon 2in1 arada حجم 350 میلی لیتر",
BrandNames = "شوریدر",
CategoryName = "شوینده سر",
Cost = 1200000,
});
return base.OnInitializedAsync();
}
private async Task AddProductClicked()
{
DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true };
var options = new DialogOptions { CloseOnEscapeKey = true , Position = DialogPosition.Center, MaxWidth = MaxWidth.Large , DisableBackdropClick = true};
await DialogService.ShowAsync<ProductActionDialogBox>("افزودن محصول جدید", maxWidth);
} }
} }

View File

@ -0,0 +1,183 @@
namespace NetinaShop.AdminPanel.PWA.Pages;
public class ProductsPageViewModel : BaseViewModel<ObservableCollection<ProductSDto>>
{
private readonly NavigationManager _navigationManager;
private readonly ISnackbar _snackbar;
private readonly IUserUtility _userUtility;
private readonly IDialogService _dialogService;
private readonly IRestWrapper _restWrapper;
public string Search = string.Empty;
public int CurrentPage = 0;
public int PageCount = 1;
public ProductsPageViewModel(NavigationManager navigationManager, ISnackbar snackbar, IUserUtility userUtility, IRestWrapper restWrapper, IDialogService dialogService)
{
_navigationManager = navigationManager;
_snackbar = snackbar;
_userUtility = userUtility;
_restWrapper = restWrapper;
_dialogService = dialogService;
}
public override async Task InitializeAsync()
{
try
{
IsProcessing = true;
PageDto.Clear();
var dto = await _restWrapper.CrudDtoApiRest<Product, ProductSDto, Guid>(Address.ProductController)
.ReadAll(CurrentPage);
dto.ForEach(d => PageDto.Add(d));
if (PageDto.Count == 20)
PageCount = 2;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
await base.InitializeAsync();
}
public async Task ChangePageAsync(int page)
{
CurrentPage = page - 1;
if (CurrentPage > PageCount - 2)
{
try
{
IsProcessing = true;
List<ProductSDto> dto = new List<ProductSDto>();
if (Search.IsNullOrEmpty())
{
dto = await _restWrapper.CrudDtoApiRest<Product, ProductSDto, Guid>(Address.ProductController)
.ReadAll(CurrentPage);
}
else
{
dto = await _restWrapper.ProductRestApi.ReadAll(CurrentPage, Search);
}
dto.ForEach(d => PageDto.Add(d));
if (PageDto.Count % 20 == 0)
PageCount = CurrentPage + 2;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}
public async Task AddProductClicked()
{
DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true };
await _dialogService.ShowAsync<ProductActionDialogBox>("افزودن محصول جدید", maxWidth);
}
public async Task EditProductClicked(ProductSDto product)
{
DialogOptions maxWidth = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true };
var parameters = new DialogParameters<ProductActionDialogBox>();
parameters.Add(x => x.Product, product);
await _dialogService.ShowAsync<ProductActionDialogBox>($"ویرایش محصول {product.PersianName}", parameters, maxWidth);
}
public async Task DeleteProductAsync(Guid selectedCategoryId)
{
var options = new DialogOptions { CloseOnEscapeKey = true };
var parameters = new DialogParameters<QuestionDialog>();
parameters.Add(x => x.ContentText, "آیا از حذف محصول اطمینان دارید ?");
var dialogReference = await _dialogService.ShowAsync<QuestionDialog>("حذف شرح حال", parameters, options);
var result = await dialogReference.Result;
if (!result.Canceled)
{
try
{
IsProcessing = true;
var token = await _userUtility.GetBearerTokenAsync();
await _restWrapper.CrudDtoApiRest<Product, ProductSDto, Guid>(Address.ProductController)
.Delete(selectedCategoryId, token);
_snackbar.Add("حذف محصول با موفقیت انجام شد", Severity.Success);
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}
public async Task SearchChanged(string search)
{
if (search.IsNullOrEmpty() && !Search.IsNullOrEmpty())
await InitializeAsync();
Search = search;
}
public async Task SearchAsync()
{
try
{
if (Search.IsNullOrEmpty())
throw new AppException("دسته بندی برای جست جو وارد نشده است");
IsProcessing = true;
CurrentPage = 0;
PageCount = 1;
PageDto.Clear();
var dto = await _restWrapper.ProductRestApi.ReadAll(CurrentPage, Search);
dto.ForEach(d => PageDto.Add(d));
if (PageDto.Count == 20)
PageCount = 2;
}
catch (ApiException ex)
{
var exe = await ex.GetContentAsAsync<ApiResult>();
_snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error);
}
catch (Exception e)
{
_snackbar.Add(e.Message, Severity.Error);
}
finally
{
IsProcessing = false;
}
}
}

View File

@ -31,5 +31,7 @@ builder.Services.AddMudServices(config =>
builder.Services.AddScoped<IRestWrapper, RestWrapper>(); builder.Services.AddScoped<IRestWrapper, RestWrapper>();
builder.Services.AddScoped<IUserUtility, UserUtility>(); builder.Services.AddScoped<IUserUtility, UserUtility>();
builder.Services.AddBlazoredLocalStorage(); builder.Services.AddBlazoredLocalStorage();
builder.Services.AddScoped<Radzen.ContextMenuService>();
builder.Services.AddScoped<Radzen.DialogService>();
builder.Services.AddPWAUpdater(); builder.Services.AddPWAUpdater();
await builder.Build().RunAsync(); await builder.Build().RunAsync();

View File

@ -1,5 +1,4 @@
using System.Security.Claims; using System.Security.Claims;
using Blazorise.Extensions;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
namespace NetinaShop.AdminPanel.PWA.Services; namespace NetinaShop.AdminPanel.PWA.Services;
@ -19,14 +18,17 @@ public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{ {
return new AuthenticationState(new()); return new AuthenticationState(new());
} }
var user = await _userUtility.GetUserAsync();
var identity = new ClaimsIdentity(new[] var identity = new ClaimsIdentity(new[]
{ {
new Claim(ClaimTypes.Name, "mrfibuli"), new Claim(ClaimTypes.Name, user.FirstName + " " + user.LastName),
}, "Custom Authentication"); new Claim(ClaimTypes.MobilePhone , user.PhoneNumber)
}, "Bearer");
var user = new ClaimsPrincipal(identity); var claimUser = new ClaimsPrincipal(identity);
return new AuthenticationState(user); return new AuthenticationState(claimUser);
} }
} }

View File

@ -3,7 +3,7 @@
public interface IAuthRestApi public interface IAuthRestApi
{ {
[Get("/verifycode")] [Get("/verifycode")]
public Task<SignUpStatus> GetVerifyCodeAsync([Query] string phoneNumber); public Task GetVerifyCodeAsync([Query] string phoneNumber);
[Post("/login/code")] [Post("/login/code")]
public Task<AccessToken<ApplicationUserSDto>> LoginWithVerifyCodeAsync([Body] LoginRequestDto request); public Task<AccessToken<ApplicationUserSDto>> LoginWithVerifyCodeAsync([Body] LoginRequestDto request);

View File

@ -14,7 +14,7 @@ public interface ICrudApiRest<T, in TKey> where T : class
Task<T> ReadOne(TKey key, [Header("Authorization")] string authorization); Task<T> ReadOne(TKey key, [Header("Authorization")] string authorization);
[Put("")] [Put("")]
Task Update([Body] T payload, [Header("Authorization")] string authorization); Task Update<TUpdateCommand>([Body] TUpdateCommand payload, [Header("Authorization")] string authorization);
[Delete("/{key}")] [Delete("/{key}")]
Task Delete(TKey key, [Header("Authorization")] string authorization); Task Delete(TKey key, [Header("Authorization")] string authorization);

View File

@ -0,0 +1,14 @@
namespace NetinaShop.AdminPanel.PWA.Services.RestServices;
public interface IProductCategoryRestApi
{
[Get("")]
Task<List<ProductCategorySDto>> ReadAll();
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] int page);
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] int page,[Query]string categoryName);
[Get("")]
Task<List<ProductCategorySDto>> ReadAll([Query] string categoryName);
}

View File

@ -0,0 +1,12 @@
namespace NetinaShop.AdminPanel.PWA.Services.RestServices;
public interface IProductRestApi
{
[Get("")]
Task<List<ProductSDto>> ReadAll();
[Get("")]
Task<List<ProductSDto>> ReadAll([Query] int page);
[Get("")]
Task<List<ProductSDto>> ReadAll([Query] int page, [Query] string productName);
}

View File

@ -8,4 +8,6 @@ public interface IRestWrapper
public IAuthRestApi AuthRestApi { get; } public IAuthRestApi AuthRestApi { get; }
public IUserRestApi UserRestApi { get; } public IUserRestApi UserRestApi { get; }
public IProductCategoryRestApi ProductCategoryRestApi { get; }
public IProductRestApi ProductRestApi { get; }
} }

View File

@ -20,4 +20,6 @@ public class RestWrapper : IRestWrapper
} }
public IAuthRestApi AuthRestApi => RestService.For<IAuthRestApi>(Address.AuthController, setting); public IAuthRestApi AuthRestApi => RestService.For<IAuthRestApi>(Address.AuthController, setting);
public IUserRestApi UserRestApi => RestService.For<IUserRestApi>(Address.UserController, setting); public IUserRestApi UserRestApi => RestService.For<IUserRestApi>(Address.UserController, setting);
public IProductCategoryRestApi ProductCategoryRestApi => RestService.For<IProductCategoryRestApi>(Address.ProductCategoryController, setting);
public IProductRestApi ProductRestApi => RestService.For<IProductRestApi>(Address.ProductController, setting);
} }

View File

@ -501,6 +501,10 @@ video {
--tw-backdrop-saturate: ; --tw-backdrop-saturate: ;
--tw-backdrop-sepia: ; --tw-backdrop-sepia: ;
} }
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mx-4 { .mx-4 {
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
@ -513,6 +517,10 @@ video {
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-auto { .my-auto {
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
@ -520,12 +528,24 @@ video {
.-ml-4 { .-ml-4 {
margin-left: -1rem; margin-left: -1rem;
} }
.-ml-6 {
margin-left: -1.5rem;
}
.-mr-2 {
margin-right: -0.5rem;
}
.-mt-3 { .-mt-3 {
margin-top: -0.75rem; margin-top: -0.75rem;
} }
.mb-10 { .mb-10 {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-5 { .mb-5 {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
@ -535,6 +555,12 @@ video {
.mr-2 { .mr-2 {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.mt-2 {
margin-top: 0.5rem;
}
.mt-2\.5 {
margin-top: 0.625rem;
}
.mt-3 { .mt-3 {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@ -544,37 +570,115 @@ video {
.mt-5 { .mt-5 {
margin-top: 1.25rem; margin-top: 1.25rem;
} }
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.flex { .flex {
display: flex; display: flex;
} }
.h-10 {
height: 2.5rem;
}
.h-12 {
height: 3rem;
}
.h-5 {
height: 1.25rem;
}
.h-64 {
height: 16rem;
}
.h-7 {
height: 1.75rem;
}
.h-full {
height: 100%;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
.min-h-\[10rem\] {
min-height: 10rem;
}
.w-10 {
width: 2.5rem;
}
.w-12 {
width: 3rem;
}
.w-5 {
width: 1.25rem;
}
.w-64 {
width: 16rem;
}
.w-7 {
width: 1.75rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
.w-screen { .w-screen {
width: 100vw; width: 100vw;
} }
.basis-full {
flex-basis: 100%;
}
.flex-row { .flex-row {
flex-direction: row; flex-direction: row;
} }
.flex-col-reverse {
flex-direction: column-reverse;
}
.items-center {
align-items: center;
}
.justify-end { .justify-end {
justify-content: flex-end; justify-content: flex-end;
} }
.overflow-hidden {
overflow: hidden;
}
.rounded-md {
border-radius: 0.375rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
.bg-\[\#000000\] {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.bg-\[\#EEEEEE\] {
--tw-bg-opacity: 1;
background-color: rgb(238 238 238 / var(--tw-bg-opacity));
}
.bg-\[--color-background\] { .bg-\[--color-background\] {
background-color: var(--color-background); background-color: var(--color-background);
} }
.bg-transparent {
background-color: transparent;
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-opacity-20 {
--tw-bg-opacity: 0.2;
}
.p-0 {
padding: 0px;
}
.p-8 { .p-8 {
padding: 2rem; padding: 2rem;
} }
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 { .py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@ -583,12 +687,43 @@ video {
padding-top: 0.75rem; padding-top: 0.75rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pt-8 { .pt-8 {
padding-top: 2rem; padding-top: 2rem;
} }
.text-center {
text-align: center;
}
.text-justify {
text-align: justify;
}
.text-\[1\.8rem\] {
font-size: 1.8rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-xl {
font-size: 1.25rem;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
.font-extrabold {
font-weight: 800;
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
@font-face { @font-face {
font-family: iranyekan; font-family: iranyekan;
@ -774,3 +909,69 @@ a, .btn-link {
code { code {
color: #c02d76; color: #c02d76;
} }
.focus\:border-teal-500:focus {
--tw-border-opacity: 1;
border-color: rgb(20 184 166 / var(--tw-border-opacity));
}
.focus\:ring-teal-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(20 184 166 / var(--tw-ring-opacity));
}
@media (min-width: 640px) {
.sm\:mt-6 {
margin-top: 1.5rem;
}
.sm\:py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
}
@media (min-width: 768px) {
.md\:basis-1\/2 {
flex-basis: 50%;
}
.md\:flex-row {
flex-direction: row;
}
.md\:rounded-bl-xl {
border-bottom-left-radius: 0.75rem;
}
.md\:rounded-tl-xl {
border-top-left-radius: 0.75rem;
}
.md\:rounded-tr-none {
border-top-right-radius: 0px;
}
.md\:px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
}
@media (min-width: 1024px) {
.lg\:h-96 {
height: 24rem;
}
.lg\:w-96 {
width: 24rem;
}
.lg\:px-20 {
padding-left: 5rem;
padding-right: 5rem;
}
}

View File

@ -554,6 +554,11 @@ video {
--tw-backdrop-sepia: ; --tw-backdrop-sepia: ;
} }
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mx-4 { .mx-4 {
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
@ -569,6 +574,11 @@ video {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-auto { .my-auto {
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
@ -578,6 +588,14 @@ video {
margin-left: -1rem; margin-left: -1rem;
} }
.-ml-6 {
margin-left: -1.5rem;
}
.-mr-2 {
margin-right: -0.5rem;
}
.-mt-3 { .-mt-3 {
margin-top: -0.75rem; margin-top: -0.75rem;
} }
@ -586,6 +604,14 @@ video {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mb-5 { .mb-5 {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
@ -598,6 +624,14 @@ video {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.mt-2 {
margin-top: 0.5rem;
}
.mt-2\.5 {
margin-top: 0.625rem;
}
.mt-3 { .mt-3 {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@ -610,14 +644,69 @@ video {
margin-top: 1.25rem; margin-top: 1.25rem;
} }
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.flex { .flex {
display: flex; display: flex;
} }
.h-10 {
height: 2.5rem;
}
.h-12 {
height: 3rem;
}
.h-5 {
height: 1.25rem;
}
.h-64 {
height: 16rem;
}
.h-7 {
height: 1.75rem;
}
.h-full {
height: 100%;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
.min-h-\[10rem\] {
min-height: 10rem;
}
.w-10 {
width: 2.5rem;
}
.w-12 {
width: 3rem;
}
.w-5 {
width: 1.25rem;
}
.w-64 {
width: 16rem;
}
.w-7 {
width: 1.75rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -626,31 +715,78 @@ video {
width: 100vw; width: 100vw;
} }
.basis-full {
flex-basis: 100%;
}
.flex-row { .flex-row {
flex-direction: row; flex-direction: row;
} }
.flex-col-reverse {
flex-direction: column-reverse;
}
.items-center {
align-items: center;
}
.justify-end { .justify-end {
justify-content: flex-end; justify-content: flex-end;
} }
.overflow-hidden {
overflow: hidden;
}
.rounded-md {
border-radius: 0.375rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
.bg-\[\#000000\] {
--tw-bg-opacity: 1;
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
}
.bg-\[\#EEEEEE\] {
--tw-bg-opacity: 1;
background-color: rgb(238 238 238 / var(--tw-bg-opacity));
}
.bg-\[--color-background\] { .bg-\[--color-background\] {
background-color: var(--color-background); background-color: var(--color-background);
} }
.bg-transparent {
background-color: transparent;
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-opacity-20 {
--tw-bg-opacity: 0.2;
}
.p-0 {
padding: 0px;
}
.p-8 { .p-8 {
padding: 2rem; padding: 2rem;
} }
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 { .py-2 {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@ -661,14 +797,54 @@ video {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.pt-8 { .pt-8 {
padding-top: 2rem; padding-top: 2rem;
} }
.text-center {
text-align: center;
}
.text-justify {
text-align: justify;
}
.text-\[1\.8rem\] {
font-size: 1.8rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-xl {
font-size: 1.25rem;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
.font-extrabold {
font-weight: 800;
}
.text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
@font-face { @font-face {
font-family: iranyekan; font-family: iranyekan;
@ -884,3 +1060,66 @@ a, .btn-link {
code { code {
color: #c02d76; color: #c02d76;
} }
.focus\:border-teal-500:focus {
--tw-border-opacity: 1;
border-color: rgb(20 184 166 / var(--tw-border-opacity));
}
.focus\:ring-teal-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(20 184 166 / var(--tw-ring-opacity));
}
@media (min-width: 640px) {
.sm\:mt-6 {
margin-top: 1.5rem;
}
.sm\:py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
}
@media (min-width: 768px) {
.md\:basis-1\/2 {
flex-basis: 50%;
}
.md\:flex-row {
flex-direction: row;
}
.md\:rounded-bl-xl {
border-bottom-left-radius: 0.75rem;
}
.md\:rounded-tl-xl {
border-top-left-radius: 0.75rem;
}
.md\:rounded-tr-none {
border-top-right-radius: 0px;
}
.md\:px-10 {
padding-left: 2.5rem;
padding-right: 2.5rem;
}
}
@media (min-width: 1024px) {
.lg\:h-96 {
height: 24rem;
}
.lg\:w-96 {
width: 24rem;
}
.lg\:px-20 {
padding-left: 5rem;
padding-right: 5rem;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,19 @@
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" /> <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css"> <link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
<!-- Main Quill library -->
<script src="//cdn.quilljs.com/1.3.6/quill.js"></script>
<script src="//cdn.quilljs.com/1.3.6/quill.min.js"></script>
<!-- Theme included stylesheets -->
<link href="//cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<link href="//cdn.quilljs.com/1.3.6/quill.bubble.css" rel="stylesheet">
<!-- Core build with no theme, formatting, non-essential modules -->
<link href="//cdn.quilljs.com/1.3.6/quill.core.css" rel="stylesheet">
<script src="//cdn.quilljs.com/1.3.6/quill.core.js"></script>
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE"> <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
</head> </head>