Add product categories and full information display

This commit is contained in:
masoodafar-web
2025-11-28 10:13:46 +03:30
parent 82ddafda33
commit fe5f7bd9b9
4 changed files with 116 additions and 16 deletions

View File

@@ -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>

View File

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

View File

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

View File

@@ -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=",