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
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:
@@ -56,6 +56,7 @@ public static class ConfigureServices
|
|||||||
services.AddSingleton<UserAuthInfo>();
|
services.AddSingleton<UserAuthInfo>();
|
||||||
services.AddScoped<AuthService>();
|
services.AddScoped<AuthService>();
|
||||||
services.AddScoped<AuthDialogService>();
|
services.AddScoped<AuthDialogService>();
|
||||||
|
services.AddScoped<GuestActionGate>();
|
||||||
// Storefront services
|
// Storefront services
|
||||||
services.AddScoped<CartService>();
|
services.AddScoped<CartService>();
|
||||||
services.AddScoped<ProductService>();
|
services.AddScoped<ProductService>();
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Success" Size="Size.Large" />
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
|
<MudText Typo="Typo.subtitle1">شارژ کیف پول فروشگاه اعتباری</MudText>
|
||||||
<MudText Typo="Typo.caption" Color="Color.Default">شارژ ۵۶ میلیون تومان کیف پول فروشگاه اعتباری</MudText>
|
<MudText Typo="Typo.caption" Color="Color.Default">شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ public partial class Cart : IDisposable
|
|||||||
{
|
{
|
||||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
DiscountCart.OnChange += StateHasChanged;
|
DiscountCart.OnChange += StateHasChanged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public partial class Checkout
|
|||||||
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
[Inject] private DiscountOrderService DiscountOrderService { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
|
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
private List<CustomerAddressModel> _addresses = new();
|
||||||
private CustomerAddressModel? _selectedAddress;
|
private CustomerAddressModel? _selectedAddress;
|
||||||
@@ -32,6 +34,10 @@ public partial class Checkout
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
await VAT.LoadAsync();
|
await VAT.LoadAsync();
|
||||||
await DiscountCart.EnsureInitializedAsync();
|
await DiscountCart.EnsureInitializedAsync();
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public partial class ProductDetail
|
|||||||
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
[Inject] private DiscountProductService DiscountProductService { get; set; } = default!;
|
||||||
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCartService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
|
|
||||||
private DiscountProductDetail? _product;
|
private DiscountProductDetail? _product;
|
||||||
private string _selectedImage = string.Empty;
|
private string _selectedImage = string.Empty;
|
||||||
@@ -59,8 +60,14 @@ public partial class ProductDetail
|
|||||||
_addingToCart = true;
|
_addingToCart = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DiscountCartService.AddAsync(_product.Id, _quantity);
|
var productId = _product.Id;
|
||||||
Snackbar.Add($"{_product.Title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
var qty = _quantity;
|
||||||
|
var title = _product.Title;
|
||||||
|
await GuestGate.RunAsync(async () =>
|
||||||
|
{
|
||||||
|
await DiscountCartService.AddAsync(productId, qty);
|
||||||
|
Snackbar.Add($"{title} به سبد خرید اضافه شد", MudBlazor.Severity.Success);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
[Inject] private DiscountProductService ProductService { get; set; } = default!;
|
[Inject] private DiscountProductService ProductService { get; set; } = default!;
|
||||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||||
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
|
|
||||||
private string _search = string.Empty;
|
private string _search = string.Empty;
|
||||||
private long? _selectedCategoryId;
|
private long? _selectedCategoryId;
|
||||||
@@ -94,7 +95,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private async Task AddToCart(DiscountProductCard p)
|
private async Task AddToCart(DiscountProductCard p)
|
||||||
{
|
{
|
||||||
await DiscountCart.AddAsync(p.Id);
|
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToProduct(long id)
|
private void NavigateToProduct(long id)
|
||||||
|
|||||||
@@ -93,6 +93,161 @@
|
|||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@* ═══════════════════════════════════════════════
|
||||||
|
1c. TOP-SELLING REGULAR PRODUCTS
|
||||||
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_loadingTopProducts)
|
||||||
|
{
|
||||||
|
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
|
<MudStack AlignItems="AlignItems.Center" Class="py-4">
|
||||||
|
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
|
||||||
|
</MudStack>
|
||||||
|
</MudContainer>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@if (_topRegularProducts.Any())
|
||||||
|
{
|
||||||
|
<section class="section-landing" style="background:var(--mud-palette-background-gray);">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
|
<div class="text-center mb-6 fade-in-up">
|
||||||
|
<MudText Typo="Typo.h3">پرخریدترین محصولات فروشگاه</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||||
|
محبوبترین محصولات کارا بازار سلامت
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||||
|
@foreach (var p in _topRegularProducts)
|
||||||
|
{
|
||||||
|
<MudItem xs="6" sm="6" md="4">
|
||||||
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||||
|
Style="cursor:pointer;"
|
||||||
|
@onclick="() => NavigateToRegularProduct(p.Id)">
|
||||||
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
|
<div style="aspect-ratio:1/1;width:100%;background-image:url('@p.ImageUrl');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||||
|
@if (p.RemainingCount <= 0)
|
||||||
|
{
|
||||||
|
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||||
|
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (p.RemainingCount <= 5)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||||
|
Style="position:absolute;top:8px;right:8px;">
|
||||||
|
فقط @p.RemainingCount عدد
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||||
|
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatPrice(p.Price)</MudText>
|
||||||
|
</div>
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||||
|
Disabled="@(p.RemainingCount <= 0)"
|
||||||
|
OnClick="@(() => AddRegularToCart(p))">
|
||||||
|
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||||
|
</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="rounded-pill"
|
||||||
|
Href="@RouteConstants.Store.Products"
|
||||||
|
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||||
|
مشاهده همه محصولات
|
||||||
|
</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudContainer>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* ═══════════════════════════════════════════════
|
||||||
|
1d. TOP-SELLING DISCOUNT STORE PRODUCTS
|
||||||
|
═══════════════════════════════════════════════ *@
|
||||||
|
@if (_topDiscountProducts.Any())
|
||||||
|
{
|
||||||
|
<section class="section-landing">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large">
|
||||||
|
<div class="text-center mb-6 fade-in-up">
|
||||||
|
<MudText Typo="Typo.h3">پرخریدترین محصولات فروشگاه اعتباری</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Class="mud-text-secondary mt-2">
|
||||||
|
محصولات ویژه اعضای باشگاه با پرداخت اعتباری
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MudGrid Spacing="2" Justify="Justify.FlexStart">
|
||||||
|
@foreach (var p in _topDiscountProducts)
|
||||||
|
{
|
||||||
|
<MudItem xs="6" sm="6" md="4">
|
||||||
|
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden landing-product-card"
|
||||||
|
Style="cursor:pointer;"
|
||||||
|
@onclick="() => NavigateToDiscountProduct(p.Id)">
|
||||||
|
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||||
|
<div style="aspect-ratio:1/1;width:100%;background-image:url('@(string.IsNullOrWhiteSpace(p.ThumbnailUrl) ? p.ImageUrl : p.ThumbnailUrl)');background-size:cover;background-position:center;border-radius:0.5rem;position:relative;">
|
||||||
|
@if (p.RemainingCount <= 0)
|
||||||
|
{
|
||||||
|
<div style="position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;border-radius:0.5rem;">
|
||||||
|
<MudChip T="string" Color="Color.Error" Variant="Variant.Filled" Size="Size.Small">ناموجود</MudChip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (p.RemainingCount <= 5)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small"
|
||||||
|
Style="position:absolute;top:8px;right:8px;">
|
||||||
|
فقط @p.RemainingCount عدد
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
@if (p.MaxDiscountPercent > 0)
|
||||||
|
{
|
||||||
|
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Filled" Size="Size.Small"
|
||||||
|
Style="position:absolute;top:8px;left:8px;">
|
||||||
|
@p.MaxDiscountPercent% اعتبار
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="pa-1 flex-grow-1 d-flex flex-column justify-space-between">
|
||||||
|
<MudText Typo="Typo.subtitle1" Style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">@p.Title</MudText>
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.subtitle2" Color="Color.Primary">@FormatDiscountPrice(p.Price)</MudText>
|
||||||
|
<MudText Typo="Typo.overline" Class="mud-text-secondary" Style="font-size:0.6rem;line-height:1;">(+ ارزش افزوده)</MudText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MudCardContent>
|
||||||
|
<MudCardActions Class="mt-auto pa-2" @onclick:stopPropagation="true">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" FullWidth="true"
|
||||||
|
StartIcon="@Icons.Material.Filled.AddShoppingCart"
|
||||||
|
Disabled="@(p.RemainingCount <= 0)"
|
||||||
|
OnClick="@(() => AddDiscountToCart(p))">
|
||||||
|
@(p.RemainingCount <= 0 ? "ناموجود" : "افزودن به سبد")
|
||||||
|
</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" Class="rounded-pill"
|
||||||
|
Href="@RouteConstants.DiscountStore.Products"
|
||||||
|
EndIcon="@Icons.Material.Filled.ArrowBack">
|
||||||
|
مشاهده همه محصولات اعتباری
|
||||||
|
</MudButton>
|
||||||
|
</div>
|
||||||
|
</MudContainer>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@* ═══════════════════════════════════════════════
|
@* ═══════════════════════════════════════════════
|
||||||
2. HOW IT WORKS — 3 numbered steps
|
2. HOW IT WORKS — 3 numbered steps
|
||||||
═══════════════════════════════════════════════ *@
|
═══════════════════════════════════════════════ *@
|
||||||
|
|||||||
@@ -10,11 +10,22 @@ public partial class Index : IDisposable
|
|||||||
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
[Inject] private BlogPostService BlogPostService { get; set; } = default!;
|
||||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
[Inject] private SitePageSettingsService PageSettingsService { 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 ──
|
// ── CMS page data ──
|
||||||
private PageSettingsDto? _pageData;
|
private PageSettingsDto? _pageData;
|
||||||
private LandingSettings? _settings;
|
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) ──
|
// ── Latest blog posts (loaded from CMS) ──
|
||||||
private List<BlogPostCardDto> _latestPosts = new();
|
private List<BlogPostCardDto> _latestPosts = new();
|
||||||
|
|
||||||
@@ -51,10 +62,29 @@ public partial class Index : IDisposable
|
|||||||
PopulateFromSettings();
|
PopulateFromSettings();
|
||||||
_dataLoaded = true;
|
_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
|
// Load latest published blog posts
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_latestPosts = await BlogPostService.GetFeaturedPostsAsync(2);
|
_latestPosts = featuredPostsTask.IsCompletedSuccessfully
|
||||||
|
? featuredPostsTask.Result
|
||||||
|
: await BlogPostService.GetFeaturedPostsAsync(2);
|
||||||
|
|
||||||
if (_latestPosts.Count < 2)
|
if (_latestPosts.Count < 2)
|
||||||
{
|
{
|
||||||
@@ -267,6 +297,21 @@ public partial class Index : IDisposable
|
|||||||
Navigation.NavigateTo($"/blog/{slug}");
|
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()
|
private async void OnStateChanged()
|
||||||
{
|
{
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ public partial class Cart : ComponentBase, IDisposable
|
|||||||
{
|
{
|
||||||
[Inject] private CartService CartService { get; set; } = default!;
|
[Inject] private CartService CartService { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
// Navigation and Snackbar are available via _Imports.razor
|
// Navigation and Snackbar are available via _Imports.razor
|
||||||
private CartService CartData => CartService;
|
private CartService CartData => CartService;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||||
await CartService.EnsureInitializedAsync();
|
await CartService.EnsureInitializedAsync();
|
||||||
CartService.OnChange += StateHasChanged;
|
CartService.OnChange += StateHasChanged;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
[Inject] private UserAddressContract.UserAddressContractClient UserAddressContract { get; set; } = default!;
|
||||||
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
[Inject] private UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
|
||||||
|
[Inject] private AuthDialogService AuthDialogService { get; set; } = default!;
|
||||||
|
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||||
// Snackbar and Navigation are injected via _Imports.razor
|
// Snackbar and Navigation are injected via _Imports.razor
|
||||||
|
|
||||||
private List<CustomerAddressModel> _addresses = new();
|
private List<CustomerAddressModel> _addresses = new();
|
||||||
@@ -27,6 +29,10 @@ public partial class CheckoutSummary : ComponentBase
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
|
if (!await AuthService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await AuthDialogService.ShowAuthDialogAsync();
|
||||||
|
}
|
||||||
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
// لود سبد خرید (فقط اگر کاربر لاگین کرده باشد)
|
||||||
await Cart.EnsureInitializedAsync();
|
await Cart.EnsureInitializedAsync();
|
||||||
await LoadAddresses();
|
await LoadAddresses();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||||
[Inject] private CartService Cart { get; set; } = default!;
|
[Inject] private CartService Cart { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter] public long id { get; set; }
|
[Parameter] public long id { get; set; }
|
||||||
|
|
||||||
@@ -92,14 +93,18 @@ public partial class ProductDetail : ComponentBase, IDisposable
|
|||||||
private async Task AddToCart()
|
private async Task AddToCart()
|
||||||
{
|
{
|
||||||
if (_product is null) return;
|
if (_product is null) return;
|
||||||
await Cart.Add(_product, 1);
|
var product = _product;
|
||||||
|
await GuestGate.RunAsync(() => Cart.Add(product, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RemoveFromCart()
|
private async Task RemoveFromCart()
|
||||||
{
|
{
|
||||||
if (_product is null) return;
|
if (_product is null) return;
|
||||||
_qty--;
|
await GuestGate.RunAsync(async () =>
|
||||||
await Cart.UpdateQuantity(CurrentCartItem.ProductId, _qty);
|
{
|
||||||
|
_qty--;
|
||||||
|
await Cart.UpdateQuantity(CurrentCartItem!.ProductId, _qty);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IncreaseLocalQty()
|
private void IncreaseLocalQty()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
[Inject] private CategoryService CategoryService { get; set; } = default!;
|
||||||
[Inject] private CartService Cart { get; set; } = default!;
|
[Inject] private CartService Cart { get; set; } = default!;
|
||||||
[Inject] private VATService VAT { get; set; } = default!;
|
[Inject] private VATService VAT { get; set; } = default!;
|
||||||
|
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||||
|
|
||||||
private string _query = string.Empty;
|
private string _query = string.Empty;
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
@@ -112,7 +113,7 @@ public partial class Products : ComponentBase, IDisposable
|
|||||||
|
|
||||||
private async Task AddToCart(Product p)
|
private async Task AddToCart(Product p)
|
||||||
{
|
{
|
||||||
await Cart.Add(p, 1);
|
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class DiscountProductService
|
|||||||
public async Task<DiscountProductListResult> GetProductsAsync(
|
public async Task<DiscountProductListResult> GetProductsAsync(
|
||||||
int page = 1, int pageSize = 12,
|
int page = 1, int pageSize = 12,
|
||||||
string? search = null, long? categoryId = null,
|
string? search = null, long? categoryId = null,
|
||||||
bool? inStock = null)
|
bool? inStock = null, string? sortBy = null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -97,6 +97,8 @@ public class DiscountProductService
|
|||||||
request.CategoryId = categoryId.Value;
|
request.CategoryId = categoryId.Value;
|
||||||
if (inStock.HasValue)
|
if (inStock.HasValue)
|
||||||
request.InStock = inStock.Value;
|
request.InStock = inStock.Value;
|
||||||
|
if (!string.IsNullOrWhiteSpace(sortBy))
|
||||||
|
request.SortBy = sortBy;
|
||||||
|
|
||||||
var response = await _productClient.GetDiscountProductsAsync(request);
|
var response = await _productClient.GetDiscountProductsAsync(request);
|
||||||
|
|
||||||
@@ -125,6 +127,9 @@ public class DiscountProductService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<DiscountProductListResult> GetTopSellingAsync(int count = 6)
|
||||||
|
=> GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc");
|
||||||
|
|
||||||
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace FrontOffice.Main.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// هر action ای که نیاز به لاگین دارد را از طریق این سرویس اجرا کنید.
|
||||||
|
/// اگر کاربر لاگین نباشد، مودال ورود نشان داده میشود.
|
||||||
|
/// پس از ورود موفق، action به صورت خودکار اجرا میشود.
|
||||||
|
/// </summary>
|
||||||
|
public class GuestActionGate
|
||||||
|
{
|
||||||
|
private readonly AuthService _authService;
|
||||||
|
private readonly AuthDialogService _authDialogService;
|
||||||
|
|
||||||
|
public GuestActionGate(AuthService authService, AuthDialogService authDialogService)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
_authDialogService = authDialogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اگر کاربر لاگین باشد action را اجرا میکند.
|
||||||
|
/// در غیر این صورت مودال لاگین را باز کرده و پس از ورود موفق، action را اجرا میکند.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true اگر action اجرا شد، false اگر کاربر لاگین نکرد.</returns>
|
||||||
|
public async Task<bool> RunAsync(Func<Task> action)
|
||||||
|
{
|
||||||
|
if (await _authService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _authDialogService.ShowAuthDialogAsync();
|
||||||
|
|
||||||
|
if (await _authService.IsAuthenticatedAsync())
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,9 @@ public class ProductService
|
|||||||
return result.Products;
|
return result.Products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<ProductListResult> GetTopSellingAsync(int count = 6)
|
||||||
|
=> GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count);
|
||||||
|
|
||||||
public async Task<ProductListResult> GetProductsPagedAsync(
|
public async Task<ProductListResult> GetProductsPagedAsync(
|
||||||
string? query = null, long? categoryId = null, string? sortBy = null,
|
string? query = null, long? categoryId = null, string? sortBy = null,
|
||||||
int page = 1, int pageSize = 12)
|
int page = 1, int pageSize = 12)
|
||||||
|
|||||||
Reference in New Issue
Block a user