From 82ddafda3355dc53992c99a3f376993c53b34944 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 28 Nov 2025 09:10:22 +0330 Subject: [PATCH] Update product service and detail view with gallery support --- src/FrontOffice.Main/FrontOffice.Main.csproj | 9 +- .../Pages/Store/ProductDetail.razor | 104 ++++++++++--- .../Pages/Store/ProductDetail.razor.cs | 141 +++++++++++++++++- .../Utilities/ProductService.cs | 63 ++++++-- 4 files changed, 279 insertions(+), 38 deletions(-) diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index 5246787..31e750d 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -14,9 +14,8 @@ - - - + + @@ -27,6 +26,10 @@ + + + + diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor index 4cb0e0f..29f6126 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor @@ -7,7 +7,7 @@ { - + } @@ -15,7 +15,9 @@ else if (_product is null) { محصول یافت نشد. - بازگشت + بازگشت + } else @@ -24,39 +26,105 @@ else - + @if (!string.IsNullOrWhiteSpace(MainImageUrl)) + { +
@MainImageAlt
+ } + else + { + + } + + @if (_galleryItems.Count > 1) + { + + @foreach (var item in _galleryItems) + { + + + + } + + }
@_product.Title @_product.Description - + @FormatPrice(_product.Price) - - افزودن به سبد - مشاهده سبد + + افزودن به + سبد + - - + - + - +
- + - - - - افزودن به سبد - مشاهده سبد - + + + + + + + @if (HasDiscount && OriginalPrice is not null) + { + + @FormatPrice(OriginalPrice.Value) + @($"٪{_product!.Discount}") + + } + @FormatPrice(TotalPrice) + + + + @if (IsInCart) + { + + + + @CurrentCartQuantity + + + + } + else + { + + + + + افزودن به سبد + + + + } + + + + } diff --git a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs index 4eefe95..31aa369 100644 --- a/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs +++ b/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs @@ -1,9 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Components; using FrontOffice.Main.Utilities; namespace FrontOffice.Main.Pages.Store; -public partial class ProductDetail : ComponentBase +public partial class ProductDetail : ComponentBase, IDisposable { [Inject] private ProductService ProductService { get; set; } = default!; [Inject] private CartService Cart { get; set; } = default!; @@ -13,19 +16,151 @@ public partial class ProductDetail : ComponentBase private Product? _product; private bool _loading; private int _qty = 1; + private const int MinQty = 1; + private const int MaxQty = 20; + private IReadOnlyList _galleryItems = Array.Empty(); + private ProductGalleryImage? _selectedGalleryImage; + private long TotalPrice => (_product?.Price ?? 0) * _qty; + private bool HasDiscount => _product is { Discount: > 0 and < 100 }; + + private long? OriginalPrice => HasDiscount && _product is not null + ? (long)Math.Round(_product.Price / (1 - (_product.Discount / 100m))) + : null; + + private CartItem? CurrentCartItem => _product is null + ? null + : Cart.Items.FirstOrDefault(i => i.ProductId == _product.Id); + + private bool IsInCart => CurrentCartItem is not null; + private int CurrentCartQuantity => CurrentCartItem?.Quantity ?? 0; protected override async Task OnParametersSetAsync() { _loading = true; _product = await ProductService.GetByIdAsync(id); _loading = false; + if (_product is not null) + { + _galleryItems = BuildGalleryItems(_product); + _selectedGalleryImage = _galleryItems.FirstOrDefault(); + _qty = Math.Clamp(CurrentCartItem?.Quantity ?? _qty, MinQty, MaxQty); + } + else + { + _galleryItems = Array.Empty(); + } + + StateHasChanged(); + } + + protected override void OnInitialized() + { + Cart.OnChange += HandleCartChanged; } private async Task AddToCart() { if (_product is null) return; - await Cart.Add(_product, _qty); + await Cart.Add(_product, 1); + } + + private async Task RemoveFromCart() + { + if (_product is null) return; + _qty--; + await Cart.UpdateQuantity(CurrentCartItem.ProductId, _qty); + } + + private void IncreaseLocalQty() + { + if (_qty < MaxQty) + { + _qty++; + } + } + + private void DecreaseLocalQty() + { + if (_qty > MinQty) + { + _qty--; + } + } + + // private async Task IncreaseCartQuantityAsync() + // { + // if (_product is null || CurrentCartItem is null) return; + // var target = Math.Min(CurrentCartItem.Quantity + 1, MaxQty); + // if (target != CurrentCartItem.Quantity) + // { + // await Cart.UpdateQuantity(_product.Id, target); + // } + // } + // + // private async Task RemoveFromCartAsync() + // { + // if (CurrentCartItem is null) return; + // await Cart.Remove(CurrentCartItem.cartId); + // } + + private void HandleCartChanged() + { + if (_product is null) return; + _qty = Math.Clamp(CurrentCartItem?.Quantity ?? MinQty, MinQty, MaxQty); + InvokeAsync(StateHasChanged); + } + + public void Dispose() + { + Cart.OnChange -= HandleCartChanged; } private static string FormatPrice(long price) => string.Format("{0:N0} تومان", price); -} + + private static IReadOnlyList BuildGalleryItems(Product product) + { + if (product.Gallery is { Count: > 0 }) + { + var result = new List(); + result.Add(new ProductGalleryImage(0, 0, product.Title, product.ImageUrl, product.ImageUrl)); + result.AddRange(product.Gallery.Where(x => !string.IsNullOrWhiteSpace(x.ImageUrl)).ToList()); + return result; + } + + if (!string.IsNullOrWhiteSpace(product.ImageUrl)) + { + return new[] + { + new ProductGalleryImage(0, 0, product.Title, product.ImageUrl, product.ImageUrl) + }; + } + + return Array.Empty(); + } + + private string MainImageUrl => _selectedGalleryImage?.ImageUrl + ?? _galleryItems.FirstOrDefault()?.ImageUrl + ?? _product?.ImageUrl + ?? string.Empty; + + private string MainImageAlt => _selectedGalleryImage?.Title ?? _product?.Title ?? "تصویر محصول"; + + private string GetThumbnailUrl(ProductGalleryImage item) + => string.IsNullOrWhiteSpace(item.ThumbnailUrl) ? item.ImageUrl : item.ThumbnailUrl; + + private string GetThumbnailBorder(ProductGalleryImage item) + => item == _selectedGalleryImage ? "2px solid rgba(30, 136, 229, 0.85)" : "1px solid rgba(0,0,0,0.12)"; + + private string GetThumbnailShadow(ProductGalleryImage item) + => item == _selectedGalleryImage ? "0 0 0 2px rgba(30, 136, 229, 0.25)" : "none"; + + private string GetThumbnailStyle(ProductGalleryImage item) + => + $"width:76px;height:76px;object-fit:cover;border:{GetThumbnailBorder(item)};box-shadow:{GetThumbnailShadow(item)};border-radius:0.5rem;"; + + private void SelectGalleryImage(ProductGalleryImage item) + { + if (_selectedGalleryImage == item) return; + _selectedGalleryImage = item; + } +} \ No newline at end of file diff --git a/src/FrontOffice.Main/Utilities/ProductService.cs b/src/FrontOffice.Main/Utilities/ProductService.cs index 5a7d3e6..1173fb1 100644 --- a/src/FrontOffice.Main/Utilities/ProductService.cs +++ b/src/FrontOffice.Main/Utilities/ProductService.cs @@ -11,18 +11,20 @@ public record Product( long Price, int Discount = 0, int Rate = 0, - int RemainingCount = 0); + int RemainingCount = 0) +{ + public IReadOnlyList Gallery { get; init; } = Array.Empty(); +} + +public record ProductGalleryImage( + long ProductGalleryId, + long ProductImageId, + string Title, + string ImageUrl, + string ThumbnailUrl); public class ProductService { - private static readonly List _seed = new() - { - new(1, "ماسک پزشکی سه‌لایه", "ماسک سه‌لایه با فیلتراسیون بالا مناسب برای محیط‌های عمومی.", "/images/store/mask.jpg", 49000), - new(2, "دستکش لاتکس", "دستکش لاتکس مناسب معاینات پزشکی، بدون پودر.", "/images/store/gloves.jpg", 89000), - new(3, "محلول ضدعفونی کننده", "محلول ضدعفونی بر پایه الکل ۷۰٪ مناسب دست و سطوح.", "/images/store/sanitizer.jpg", 69000), - new(4, "تب‌سنج دیجیتال", "تب‌سنج دیجیتال با دقت بالا و نمایشگر LCD.", "/images/store/thermometer.jpg", 299000), - new(5, "فشارسنج دیجیتال", "فشارسنج بازویی دیجیتال با حافظه داخلی.", "/images/store/bp-monitor.jpg", 1259000) - }; private readonly ConcurrentDictionary _cache = new(); private readonly ProductsContract.ProductsContractClient _client; @@ -30,7 +32,7 @@ public class ProductService public ProductService(ProductsContract.ProductsContractClient client) { _client = client; - foreach (var p in _seed) _cache[p.Id] = p; + } public async Task> GetProductsAsync(string? query = null) @@ -67,10 +69,13 @@ public class ProductService try { - // No single-get endpoint exposed: fetch list and pick - var resp = await _client.GetAllProductsByFilterAsync(new GetAllProductsByFilterRequest { Filter = new GetAllProductsByFilterFilter() }); - var list = MapAndCache(resp.Models); - return list.FirstOrDefault(x => x.Id == id); + var resp = await _client.GetProductsAsync(new GetProductsRequest { Id = id }); + if (resp == null) + { + return null; + } + + return MapAndCache(resp); } catch { @@ -99,4 +104,34 @@ public class ProductService } return list; } + + private Product MapAndCache(GetProductsResponse model) + { + var gallery = model.Gallery + .Select(item => new ProductGalleryImage( + ProductGalleryId: item.ProductGalleryId, + ProductImageId: item.ProductImageId, + Title: item.Title ?? string.Empty, + ImageUrl: BuildUrl(item.ImagePath), + ThumbnailUrl: BuildUrl(item.ImageThumbnailPath))) + .ToList(); + + var product = new Product( + Id: model.Id, + Title: model.Title ?? string.Empty, + Description: model.Description ?? string.Empty, + ImageUrl: BuildUrl(model.ImagePath), + Price: model.Price, + Discount: model.Discount, + Rate: model.Rate, + RemainingCount: model.RemainingCount) + { + Gallery = gallery + }; + + _cache[product.Id] = product; + return product; + } + + private static string BuildUrl(string? path) => string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path; }