Add product categories and full information display
This commit is contained in:
@@ -52,6 +52,26 @@ else
|
|||||||
<MudStack Spacing="2">
|
<MudStack Spacing="2">
|
||||||
<MudText Typo="Typo.h4">@_product.Title</MudText>
|
<MudText Typo="Typo.h4">@_product.Title</MudText>
|
||||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.Description</MudText>
|
<MudText Typo="Typo.body1" Class="mud-text-secondary">@_product.Description</MudText>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_product.FullInformation))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="mud-text-secondary" GutterBottom="true">
|
||||||
|
@((MarkupString)_product.FullInformation)
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
|
@if (_categoryPaths.Count > 0)
|
||||||
|
{
|
||||||
|
<MudStack Spacing="1">
|
||||||
|
<MudText Typo="Typo.caption" Class="mud-text-secondary">دستهبندیها</MudText>
|
||||||
|
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
|
||||||
|
@foreach (var categoryPath in _categoryPaths)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Variant="Variant.Outlined" Color="Color.Primary" OnClick="() => NavigateToCategory(categoryPath)">
|
||||||
|
@GetCategoryLabel(categoryPath)
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
<MudDivider Class="my-2"/>
|
<MudDivider Class="my-2"/>
|
||||||
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
<MudText Typo="Typo.h5" Color="Color.Primary">@FormatPrice(_product.Price)</MudText>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
private const int MinQty = 1;
|
private const int MinQty = 1;
|
||||||
private const int MaxQty = 20;
|
private const int MaxQty = 20;
|
||||||
private IReadOnlyList<ProductGalleryImage> _galleryItems = Array.Empty<ProductGalleryImage>();
|
private IReadOnlyList<ProductGalleryImage> _galleryItems = Array.Empty<ProductGalleryImage>();
|
||||||
|
private IReadOnlyList<ProductCategoryPathInfo> _categoryPaths = Array.Empty<ProductCategoryPathInfo>();
|
||||||
private ProductGalleryImage? _selectedGalleryImage;
|
private ProductGalleryImage? _selectedGalleryImage;
|
||||||
private long TotalPrice => (_product?.Price ?? 0) * _qty;
|
private long TotalPrice => (_product?.Price ?? 0) * _qty;
|
||||||
private bool HasDiscount => _product is { Discount: > 0 and < 100 };
|
private bool HasDiscount => _product is { Discount: > 0 and < 100 };
|
||||||
@@ -36,6 +37,13 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
Cart.OnChange += HandleCartChanged;
|
||||||
|
|
||||||
_loading = true;
|
_loading = true;
|
||||||
_product = await ProductService.GetByIdAsync(id);
|
_product = await ProductService.GetByIdAsync(id);
|
||||||
_loading = false;
|
_loading = false;
|
||||||
@@ -43,20 +51,20 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
_galleryItems = BuildGalleryItems(_product);
|
_galleryItems = BuildGalleryItems(_product);
|
||||||
_selectedGalleryImage = _galleryItems.FirstOrDefault();
|
_selectedGalleryImage = _galleryItems.FirstOrDefault();
|
||||||
|
_categoryPaths = _product.Categories;
|
||||||
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? _qty, MinQty, MaxQty);
|
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? _qty, MinQty, MaxQty);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_galleryItems = Array.Empty<ProductGalleryImage>();
|
_galleryItems = Array.Empty<ProductGalleryImage>();
|
||||||
|
_categoryPaths = Array.Empty<ProductCategoryPathInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Cart.OnChange += HandleCartChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddToCart()
|
private async Task AddToCart()
|
||||||
{
|
{
|
||||||
@@ -163,4 +171,13 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
if (_selectedGalleryImage == item) return;
|
if (_selectedGalleryImage == item) return;
|
||||||
_selectedGalleryImage = item;
|
_selectedGalleryImage = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void NavigateToCategory(ProductCategoryPathInfo path)
|
||||||
|
{
|
||||||
|
var target = $"{RouteConstants.Store.Products}?category={path.CategoryId}";
|
||||||
|
Navigation.NavigateTo(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCategoryLabel(ProductCategoryPathInfo path)
|
||||||
|
=> path.DisplayLabel;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using FrontOffice.BFF.Products.Protobuf.Protos.Products;
|
using FrontOffice.BFF.Products.Protobuf.Protos.Products;
|
||||||
|
|
||||||
namespace FrontOffice.Main.Utilities;
|
namespace FrontOffice.Main.Utilities;
|
||||||
@@ -7,13 +9,18 @@ public record Product(
|
|||||||
long Id,
|
long Id,
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
|
string FullInformation,
|
||||||
string ImageUrl,
|
string ImageUrl,
|
||||||
long Price,
|
long Price,
|
||||||
int Discount = 0,
|
int Discount = 1,
|
||||||
int Rate = 0,
|
int Rate = 0,
|
||||||
int RemainingCount = 0)
|
int RemainingCount = 0)
|
||||||
{
|
{
|
||||||
public IReadOnlyList<ProductGalleryImage> Gallery { get; init; } = Array.Empty<ProductGalleryImage>();
|
public IReadOnlyList<ProductGalleryImage> Gallery { get; init; }
|
||||||
|
= [];
|
||||||
|
|
||||||
|
public IReadOnlyList<ProductCategoryPathInfo> Categories { get; init; }
|
||||||
|
= [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ProductGalleryImage(
|
public record ProductGalleryImage(
|
||||||
@@ -23,16 +30,28 @@ public record ProductGalleryImage(
|
|||||||
string ImageUrl,
|
string ImageUrl,
|
||||||
string ThumbnailUrl);
|
string ThumbnailUrl);
|
||||||
|
|
||||||
|
public record ProductCategoryNodeInfo(
|
||||||
|
long Id,
|
||||||
|
string Title,
|
||||||
|
long? ParentId);
|
||||||
|
|
||||||
|
public record ProductCategoryPathInfo(
|
||||||
|
long CategoryId,
|
||||||
|
string Title,
|
||||||
|
IReadOnlyList<ProductCategoryNodeInfo> Nodes)
|
||||||
|
{
|
||||||
|
public string DisplayLabel => string.Join(" › ", Nodes.Select(node => node.Title));
|
||||||
|
public ProductCategoryNodeInfo Leaf => Nodes.Last();
|
||||||
|
}
|
||||||
|
|
||||||
public class ProductService
|
public class ProductService
|
||||||
{
|
{
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<long, Product> _cache = new();
|
private readonly ConcurrentDictionary<long, Product> _cache = new();
|
||||||
private readonly ProductsContract.ProductsContractClient _client;
|
private readonly ProductsContract.ProductsContractClient _client;
|
||||||
|
|
||||||
public ProductService(ProductsContract.ProductsContractClient client)
|
public ProductService(ProductsContract.ProductsContractClient client)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Product>> GetProductsAsync(string? query = null)
|
public async Task<List<Product>> GetProductsAsync(string? query = null)
|
||||||
@@ -57,8 +76,11 @@ public class ProductService
|
|||||||
if (!string.IsNullOrWhiteSpace(query))
|
if (!string.IsNullOrWhiteSpace(query))
|
||||||
{
|
{
|
||||||
var q = query.Trim();
|
var q = query.Trim();
|
||||||
list = list.Where(p => p.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || p.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
|
list = list.Where(p =>
|
||||||
|
p.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
p.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.OrderBy(p => p.Id).ToList();
|
return list.OrderBy(p => p.Id).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +106,8 @@ public class ProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Product> MapAndCache(Google.Protobuf.Collections.RepeatedField<GetAllProductsByFilterResponseModel> models)
|
private List<Product> MapAndCache(
|
||||||
|
Google.Protobuf.Collections.RepeatedField<GetAllProductsByFilterResponseModel> models)
|
||||||
{
|
{
|
||||||
var list = new List<Product>();
|
var list = new List<Product>();
|
||||||
foreach (var m in models)
|
foreach (var m in models)
|
||||||
@@ -93,15 +116,20 @@ public class ProductService
|
|||||||
Id: m.Id,
|
Id: m.Id,
|
||||||
Title: m.Title ?? string.Empty,
|
Title: m.Title ?? string.Empty,
|
||||||
Description: m.Description ?? string.Empty,
|
Description: m.Description ?? string.Empty,
|
||||||
|
FullInformation: m.FullInformation ?? string.Empty,
|
||||||
ImageUrl: string.IsNullOrWhiteSpace(m.ImagePath) ? string.Empty : UrlUtility.DownloadUrl + m.ImagePath,
|
ImageUrl: string.IsNullOrWhiteSpace(m.ImagePath) ? string.Empty : UrlUtility.DownloadUrl + m.ImagePath,
|
||||||
Price: m.Price,
|
Price: m.Price,
|
||||||
Discount: m.Discount,
|
Discount: m.Discount,
|
||||||
Rate: m.Rate,
|
Rate: m.Rate,
|
||||||
RemainingCount: m.RemainingCount
|
RemainingCount: m.RemainingCount
|
||||||
);
|
);
|
||||||
_cache[p.Id] = p;
|
if (_cache.All(a => a.Key != p.Id))
|
||||||
|
_cache[p.Id] = p;
|
||||||
|
|
||||||
|
|
||||||
list.Add(p);
|
list.Add(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,18 +148,53 @@ public class ProductService
|
|||||||
Id: model.Id,
|
Id: model.Id,
|
||||||
Title: model.Title ?? string.Empty,
|
Title: model.Title ?? string.Empty,
|
||||||
Description: model.Description ?? string.Empty,
|
Description: model.Description ?? string.Empty,
|
||||||
|
FullInformation: model.FullInformation ?? string.Empty,
|
||||||
ImageUrl: BuildUrl(model.ImagePath),
|
ImageUrl: BuildUrl(model.ImagePath),
|
||||||
Price: model.Price,
|
Price: model.Price,
|
||||||
Discount: model.Discount,
|
Discount: model.Discount,
|
||||||
Rate: model.Rate,
|
Rate: model.Rate,
|
||||||
RemainingCount: model.RemainingCount)
|
RemainingCount: model.RemainingCount)
|
||||||
{
|
{
|
||||||
Gallery = gallery
|
Gallery = gallery,
|
||||||
|
Categories = MapCategoryPaths(model.Categories)
|
||||||
};
|
};
|
||||||
|
|
||||||
_cache[product.Id] = product;
|
_cache[product.Id] = product;
|
||||||
return product;
|
return product;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildUrl(string? path) => string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path;
|
private static string BuildUrl(string? path) =>
|
||||||
}
|
string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path;
|
||||||
|
|
||||||
|
private static IReadOnlyList<ProductCategoryPathInfo> MapCategoryPaths(IEnumerable<ProductCategoryPath>? categories)
|
||||||
|
{
|
||||||
|
if (categories is null)
|
||||||
|
{
|
||||||
|
return Array.Empty<ProductCategoryPathInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new List<ProductCategoryPathInfo>();
|
||||||
|
foreach (var category in categories)
|
||||||
|
{
|
||||||
|
var nodes = category.Path
|
||||||
|
.Select(node => new ProductCategoryNodeInfo(
|
||||||
|
Id: node.Id,
|
||||||
|
Title: node.Title ?? string.Empty,
|
||||||
|
ParentId: node.ParentId
|
||||||
|
))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (nodes.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new ProductCategoryPathInfo(
|
||||||
|
CategoryId: category.CategoryId,
|
||||||
|
Title: category.Title ?? string.Empty,
|
||||||
|
Nodes: nodes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"GwUrl": "https://fogw.kbs1.ir",
|
// "GwUrl": "https://fogw.kbs1.ir",
|
||||||
//"GwUrl": "https://localhost:34781",
|
"GwUrl": "https://localhost:34781",
|
||||||
"DownloadUrl": "https://dl.afrino.co",
|
"DownloadUrl": "https://dl.afrino.co",
|
||||||
"EncryptionSettings": {
|
"EncryptionSettings": {
|
||||||
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
"Key": "kmcQ3XTmH4mrdh8VHziuscyf8LLYjG//Kyni81nH/0E=",
|
||||||
|
|||||||
Reference in New Issue
Block a user