Files
FrontOffice/src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs
T
masoodafar-web 231da2cbaa
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m10s
feat: add top-seller product sections to landing page with guest browsing support
- Add two sections to landing page (Index.razor) below the hero:
  - Top 6 best-selling regular products (3-per-row grid)
  - Top 6 best-selling discount store products (3-per-row grid)
  - Each section has a 'More' button linking to /products and /discount-store
- Add GuestActionGate utility service: wraps auth-required actions; shows
  login modal for guests and resumes the original action after successful login
- Register GuestActionGate as scoped service in ConfigureServices
- Add GetTopSellingAsync(count) to ProductService and DiscountProductService
- Add optional sortBy parameter to DiscountProductService.GetProductsAsync
- Hybridize all product pages for guest browsing:
  - Store/Products, Store/ProductDetail: wrap AddToCart with GuestActionGate
  - DiscountStore/Products, DiscountStore/ProductDetail: wrap AddToCart with GuestActionGate
  - Store/Cart, DiscountStore/Cart, Store/CheckoutSummary: soft auth gate on page init
- Fix MembershipPage membership benefit text to be package-agnostic

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 19:48:14 +03:30

237 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components;
using FrontOffice.Main.Utilities;
using MudBlazor;
namespace FrontOffice.Main.Pages.Store;
public partial class ProductDetail : ComponentBase, IDisposable
{
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Parameter] public long id { get; set; }
private Product? _product;
private bool _loading;
private int _qty = 1;
private const int MinQty = 1;
// حداکثر تعداد قابل خرید بر اساس موجودی انبار
private int MaxQty => _product?.RemainingCount > 0 ? _product.RemainingCount : 0;
// آیا محصول موجود است
private bool IsInStock => _product is not null && _product.RemainingCount > 0;
private IReadOnlyList<ProductGalleryImage> _galleryItems = Array.Empty<ProductGalleryImage>();
private IReadOnlyList<ProductCategoryPathInfo> _categoryPaths = Array.Empty<ProductCategoryPathInfo>();
private ProductGalleryImage? _selectedGalleryImage;
private readonly List<BreadcrumbItem> _breadcrumbItems = new();
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()
{
}
protected override async Task OnInitializedAsync()
{
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
await Cart.EnsureInitializedAsync();
Cart.OnChange += HandleCartChanged;
_loading = true;
_product = await ProductService.GetByIdAsync(id);
_loading = false;
if (_product is not null)
{
_galleryItems = BuildGalleryItems(_product);
_selectedGalleryImage = _galleryItems.FirstOrDefault();
_categoryPaths = _product.Categories;
UpdateBreadcrumb();
_qty = Math.Clamp(CurrentCartItem?.Quantity ?? _qty, MinQty, MaxQty);
}
else
{
_galleryItems = Array.Empty<ProductGalleryImage>();
_categoryPaths = Array.Empty<ProductCategoryPathInfo>();
_breadcrumbItems.Clear();
}
StateHasChanged();
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// بارگذاری نرخ VAT
await VAT.LoadAsync();
}
}
private async Task AddToCart()
{
if (_product is null) return;
var product = _product;
await GuestGate.RunAsync(() => Cart.Add(product, 1));
}
private async Task RemoveFromCart()
{
if (_product is null) return;
await GuestGate.RunAsync(async () =>
{
_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 string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
private string FormatPriceWithoutVAT(long price) => $"{price:N0} تومان";
private static IReadOnlyList<ProductGalleryImage> BuildGalleryItems(Product product)
{
if (product.Gallery is { Count: > 0 })
{
var result = new List<ProductGalleryImage>();
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<ProductGalleryImage>();
}
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;
}
private void UpdateBreadcrumb()
{
_breadcrumbItems.Clear();
if (_product is null)
{
return;
}
_breadcrumbItems.Add(new BreadcrumbItem("خانه", href: RouteConstants.Store.Products));
var nodes = _categoryPaths
.OrderByDescending(path => path.Nodes.Count)
.FirstOrDefault()?.Nodes ?? Array.Empty<ProductCategoryNodeInfo>();
foreach (var node in nodes)
{
var target = $"{RouteConstants.Store.Products}?category={node.Id}";
_breadcrumbItems.Add(new BreadcrumbItem(node.Title, href: target));
}
// _breadcrumbItems.Add(new BreadcrumbItem(_product.Title, href: null, disabled: true));
}
private void NavigateToCategory(ProductCategoryPathInfo path)
{
var target = $"{RouteConstants.Store.Products}?category={path.CategoryId}";
Navigation.NavigateTo(target);
}
private string GetCategoryLabel(ProductCategoryPathInfo path)
=> path.DisplayLabel;
}