231da2cbaa
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m10s
- 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>
237 lines
7.8 KiB
C#
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;
|
|
} |