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