feat: add top-seller product sections to landing page with guest browsing support
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>
This commit is contained in:
masoodafar-web
2026-05-13 19:48:14 +03:30
parent a1d67c5865
commit 231da2cbaa
15 changed files with 299 additions and 10 deletions
+46 -1
View File
@@ -10,11 +10,22 @@ public partial class Index : IDisposable
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
[Inject] private AuthService AuthService { get; set; } = default!;
[Inject] private SitePageSettingsService PageSettingsService { get; set; } = default!;
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
[Inject] private CartService Cart { get; set; } = default!;
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
[Inject] private VATService VAT { get; set; } = default!;
// ── CMS page data ──
private PageSettingsDto? _pageData;
private LandingSettings? _settings;
// ── Top-selling product sections ──
private List<Product> _topRegularProducts = new();
private List<DiscountProductCard> _topDiscountProducts = new();
private bool _loadingTopProducts = true;
// ── Latest blog posts (loaded from CMS) ──
private List<BlogPostCardDto> _latestPosts = new();
@@ -51,10 +62,29 @@ public partial class Index : IDisposable
PopulateFromSettings();
_dataLoaded = true;
// Load top-selling products and blog posts in parallel
var topRegTask = ProductService.GetTopSellingAsync(6);
var topDiscTask = DiscountProductService.GetTopSellingAsync(6);
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
try
{
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);
_topRegularProducts = topRegTask.Result.Products;
_topDiscountProducts = topDiscTask.Result.Products;
}
catch
{
// Fallback: sections remain empty
}
_loadingTopProducts = false;
// Load latest published blog posts
try
{
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
? featuredPostsTask.Result
: await BlogPostService.GetFeaturedPostsAsync(2);
if (_latestPosts.Count < 2)
{
@@ -267,6 +297,21 @@ public partial class Index : IDisposable
Navigation.NavigateTo($"/blog/{slug}");
}
private void NavigateToRegularProduct(long id)
=> Navigation.NavigateTo(RouteConstants.Store.ProductDetail + id);
private void NavigateToDiscountProduct(long id)
=> Navigation.NavigateTo(RouteConstants.DiscountStore.ProductDetail + id);
private async Task AddRegularToCart(Product p)
=> await GuestGate.RunAsync(() => Cart.Add(p, 1));
private async Task AddDiscountToCart(DiscountProductCard p)
=> await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
private string FormatDiscountPrice(long price) => $"{price:N0} تومان";
private async void OnStateChanged()
{
await InvokeAsync(StateHasChanged);