Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 16m3s
Build and Deploy to Production / build-and-deploy (push) Failing after 16m3s
This commit is contained in:
@@ -76,6 +76,7 @@
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="3"
|
||||
onclick="@(() => NavigateToProduct(p.Id))">
|
||||
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||
Style="cursor:pointer;">
|
||||
<MudCardContent Class="d-flex flex-column pa-1 h-100">
|
||||
@@ -127,6 +128,7 @@
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace FrontOffice.Main.Pages.DiscountStore;
|
||||
@@ -10,6 +12,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
[Inject] private DiscountCartService DiscountCart { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
private string _search = string.Empty;
|
||||
@@ -20,42 +23,107 @@ public partial class Products : ComponentBase, IDisposable
|
||||
private bool _hasMore = true;
|
||||
private int _totalCount;
|
||||
private const int PageSize = 12;
|
||||
private const string DefaultSortBy = "price desc";
|
||||
|
||||
private List<DiscountProductCard> _products = new();
|
||||
private List<DiscountCategoryNode> _categories = new();
|
||||
private bool _ignoreNextLocationChange;
|
||||
private bool _pendingScrollRestore;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
_loading = true;
|
||||
await DiscountCart.EnsureInitializedAsync();
|
||||
DiscountCart.OnChange += StateHasChanged;
|
||||
var categoriesTask = ProductService.GetCategoriesAsync();
|
||||
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
|
||||
await Task.WhenAll(categoriesTask, productsTask);
|
||||
_categories = categoriesTask.Result;
|
||||
var result = productsTask.Result;
|
||||
_products = result.Products;
|
||||
_totalCount = result.TotalCount;
|
||||
_hasMore = result.CurrentPage < result.TotalPages;
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
|
||||
_categories = await ProductService.GetCategoriesAsync();
|
||||
ApplyStateFromUri();
|
||||
_loading = true;
|
||||
await LoadPages(_currentPage);
|
||||
_loading = false;
|
||||
_pendingScrollRestore = true;
|
||||
}
|
||||
|
||||
private async Task LoadInitial()
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||
{
|
||||
_pendingScrollRestore = false;
|
||||
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.DiscountKey);
|
||||
if (payload is not null)
|
||||
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private void ApplyStateFromUri()
|
||||
{
|
||||
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||
_search = state.Query;
|
||||
_selectedCategoryId = state.CategoryId;
|
||||
_currentPage = state.Pages;
|
||||
}
|
||||
|
||||
private ShopListQueryState CaptureState() => new()
|
||||
{
|
||||
Query = _search,
|
||||
CategoryId = _selectedCategoryId,
|
||||
Pages = Math.Max(1, _currentPage)
|
||||
};
|
||||
|
||||
private void SyncUrl()
|
||||
{
|
||||
var target = CaptureState().ToRelativeUrl(RouteConstants.DiscountStore.Products);
|
||||
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
_ignoreNextLocationChange = true;
|
||||
Navigation.NavigateTo(target, replace: true);
|
||||
}
|
||||
|
||||
private async Task LoadPages(int pagesToLoad)
|
||||
{
|
||||
_products.Clear();
|
||||
_hasMore = true;
|
||||
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||
var search = string.IsNullOrWhiteSpace(_search) ? null : _search;
|
||||
|
||||
for (var page = 1; page <= pagesToLoad; page++)
|
||||
{
|
||||
var result = await ProductService.GetProductsAsync(
|
||||
page: page,
|
||||
pageSize: PageSize,
|
||||
search: search,
|
||||
categoryId: _selectedCategoryId,
|
||||
sortBy: DefaultSortBy);
|
||||
|
||||
if (page == 1)
|
||||
{
|
||||
_products = result.Products;
|
||||
_totalCount = result.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_products.AddRange(result.Products);
|
||||
}
|
||||
|
||||
_currentPage = page;
|
||||
_hasMore = result.CurrentPage < result.TotalPages;
|
||||
if (!_hasMore)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadFromFilters()
|
||||
{
|
||||
_loading = true;
|
||||
_currentPage = 1;
|
||||
_products.Clear();
|
||||
StateHasChanged();
|
||||
|
||||
var result = await ProductService.GetProductsAsync(
|
||||
page: 1,
|
||||
pageSize: PageSize,
|
||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
||||
categoryId: _selectedCategoryId);
|
||||
_products = result.Products;
|
||||
_totalCount = result.TotalCount;
|
||||
_hasMore = result.CurrentPage < result.TotalPages;
|
||||
await LoadPages(1);
|
||||
SyncUrl();
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
@@ -63,37 +131,37 @@ public partial class Products : ComponentBase, IDisposable
|
||||
private async Task LoadMore()
|
||||
{
|
||||
if (_loadingMore || !_hasMore) return;
|
||||
|
||||
|
||||
_loadingMore = true;
|
||||
StateHasChanged();
|
||||
|
||||
|
||||
_currentPage++;
|
||||
var result = await ProductService.GetProductsAsync(
|
||||
page: _currentPage,
|
||||
pageSize: PageSize,
|
||||
search: string.IsNullOrWhiteSpace(_search) ? null : _search,
|
||||
categoryId: _selectedCategoryId);
|
||||
categoryId: _selectedCategoryId,
|
||||
sortBy: DefaultSortBy);
|
||||
_products.AddRange(result.Products);
|
||||
_hasMore = result.CurrentPage < result.TotalPages;
|
||||
|
||||
SyncUrl();
|
||||
|
||||
_loadingMore = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task SearchProducts() => await LoadInitial();
|
||||
private async Task SearchProducts() => await ReloadFromFilters();
|
||||
|
||||
private async Task OnSearchKeyUp(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
{
|
||||
await LoadInitial();
|
||||
}
|
||||
await ReloadFromFilters();
|
||||
}
|
||||
|
||||
private async Task OnCategoryChanged(long? value)
|
||||
{
|
||||
_selectedCategoryId = value;
|
||||
await LoadInitial();
|
||||
await ReloadFromFilters();
|
||||
}
|
||||
|
||||
private async Task AddToCart(DiscountProductCard p)
|
||||
@@ -101,15 +169,44 @@ public partial class Products : ComponentBase, IDisposable
|
||||
await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id));
|
||||
}
|
||||
|
||||
private void NavigateToProduct(long id)
|
||||
private async Task NavigateToProduct(long id)
|
||||
{
|
||||
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.DiscountKey, id);
|
||||
Navigation.NavigateTo($"{RouteConstants.DiscountStore.ProductDetail}{id}");
|
||||
}
|
||||
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
if (_ignoreNextLocationChange)
|
||||
{
|
||||
_ignoreNextLocationChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||
if (!uri.AbsolutePath.Equals(RouteConstants.DiscountStore.Products, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var incoming = ShopListQueryState.Parse(uri);
|
||||
if (incoming.Matches(CaptureState()))
|
||||
return;
|
||||
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
ApplyStateFromUri();
|
||||
_loading = true;
|
||||
await LoadPages(_currentPage);
|
||||
_loading = false;
|
||||
_pendingScrollRestore = true;
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private static string FormatPrice(long price) => $"{price:N0} تومان";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DiscountCart.OnChange -= StateHasChanged;
|
||||
Navigation.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,8 @@
|
||||
@foreach (var p in _products)
|
||||
{
|
||||
<MudItem xs="6" sm="6" md="3"
|
||||
onclick="@(() => Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{p.Id}"))">
|
||||
onclick="@(() => OpenProduct(p.Id))">
|
||||
<div id="@($"shop-product-{p.Id}")" class="h-100">
|
||||
<MudCard Class="rounded-lg h-100 d-flex flex-column overflow-hidden"
|
||||
Style="cursor:pointer;">
|
||||
|
||||
@@ -164,6 +165,7 @@
|
||||
</MudButton>
|
||||
</MudCardActions>
|
||||
</MudCard>
|
||||
</div>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@@ -2,19 +2,11 @@ using System.Linq;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.JSInterop;
|
||||
using FrontOffice.Main.Utilities;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Store;
|
||||
|
||||
public enum ProductSortOption
|
||||
{
|
||||
PriceDesc, // گرانترین (پیشفرض)
|
||||
PriceAsc, // ارزانترین
|
||||
Newest, // جدیدترین
|
||||
Title // الفبایی
|
||||
}
|
||||
|
||||
public partial class Products : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||
@@ -23,6 +15,7 @@ public partial class Products : ComponentBase, IDisposable
|
||||
[Inject] private VATService VAT { get; set; } = default!;
|
||||
[Inject] private GuestActionGate GuestGate { get; set; } = default!;
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||
|
||||
private bool _isAuthenticated;
|
||||
private string _query = string.Empty;
|
||||
@@ -35,8 +28,9 @@ public partial class Products : ComponentBase, IDisposable
|
||||
private List<Product> _products = new();
|
||||
private long? _activeCategoryId;
|
||||
private string? _activeCategoryTitle;
|
||||
|
||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc; // پیشفرض: گرانترین
|
||||
private ProductSortOption _sortOption = ProductSortOption.PriceDesc;
|
||||
private bool _ignoreNextLocationChange;
|
||||
private bool _pendingScrollRestore;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -44,80 +38,132 @@ public partial class Products : ComponentBase, IDisposable
|
||||
await Cart.EnsureInitializedAsync();
|
||||
Cart.OnChange += StateHasChanged;
|
||||
Navigation.LocationChanged += HandleLocationChanged;
|
||||
await LoadInitial();
|
||||
ApplyStateFromUri();
|
||||
await LoadPages(_currentPage);
|
||||
_pendingScrollRestore = true;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// بارگذاری نرخ VAT
|
||||
await VAT.LoadAsync();
|
||||
|
||||
if (_pendingScrollRestore && !_loading && _products.Count > 0)
|
||||
{
|
||||
_pendingScrollRestore = false;
|
||||
var payload = await ShopListScrollRestore.TakeAsync(Js, ShopListScrollRestore.StoreKey);
|
||||
if (payload is not null)
|
||||
await ShopListScrollRestore.RestoreAsync(Js, payload);
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task LoadInitial()
|
||||
private void ApplyStateFromUri()
|
||||
{
|
||||
var state = ShopListQueryState.Parse(Navigation.ToAbsoluteUri(Navigation.Uri));
|
||||
_query = state.Query;
|
||||
_sortOption = ShopListQueryState.ParseSortOption(state.Sort);
|
||||
_activeCategoryId = state.CategoryId;
|
||||
_currentPage = state.Pages;
|
||||
}
|
||||
|
||||
private ShopListQueryState CaptureState() => new()
|
||||
{
|
||||
Query = _query,
|
||||
Sort = ShopListQueryState.ToSortKey(_sortOption),
|
||||
CategoryId = _activeCategoryId,
|
||||
Pages = Math.Max(1, _currentPage)
|
||||
};
|
||||
|
||||
private void SyncUrl()
|
||||
{
|
||||
var target = CaptureState().ToRelativeUrl(RouteConstants.Store.Products);
|
||||
var currentPathAndQuery = Navigation.ToAbsoluteUri(Navigation.Uri).PathAndQuery;
|
||||
if (string.Equals(currentPathAndQuery, target, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(currentPathAndQuery, target + "/", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
_ignoreNextLocationChange = true;
|
||||
Navigation.NavigateTo(target, replace: true);
|
||||
}
|
||||
|
||||
private async Task LoadPages(int pagesToLoad)
|
||||
{
|
||||
_loading = true;
|
||||
_currentPage = 1;
|
||||
_products.Clear();
|
||||
UpdateCategoryFilterFromUri();
|
||||
var sortBy = GetSortByValue();
|
||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||
_products = result.Products;
|
||||
_hasMore = result.HasNext;
|
||||
_totalCount = result.TotalCount;
|
||||
_hasMore = true;
|
||||
pagesToLoad = Math.Max(1, pagesToLoad);
|
||||
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||
|
||||
for (var page = 1; page <= pagesToLoad; page++)
|
||||
{
|
||||
var result = await ProductService.GetProductsPagedAsync(
|
||||
_query, _activeCategoryId, sortBy, page, PageSize);
|
||||
if (page == 1)
|
||||
{
|
||||
_products = result.Products;
|
||||
_totalCount = result.TotalCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
_products.AddRange(result.Products);
|
||||
}
|
||||
|
||||
_currentPage = page;
|
||||
_hasMore = result.HasNext;
|
||||
if (!_hasMore)
|
||||
break;
|
||||
}
|
||||
|
||||
_activeCategoryTitle = _activeCategoryId is { } categoryId
|
||||
? (await CategoryService.GetByIdAsync(categoryId))?.Title
|
||||
: null;
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task ReloadFromFilters()
|
||||
{
|
||||
_currentPage = 1;
|
||||
await LoadPages(1);
|
||||
SyncUrl();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadMore()
|
||||
{
|
||||
if (_loadingMore || !_hasMore) return;
|
||||
|
||||
|
||||
_loadingMore = true;
|
||||
StateHasChanged();
|
||||
|
||||
|
||||
_currentPage++;
|
||||
var sortBy = GetSortByValue();
|
||||
var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||
var sortBy = ShopListQueryState.ToApiSortBy(_sortOption);
|
||||
var result = await ProductService.GetProductsPagedAsync(
|
||||
_query, _activeCategoryId, sortBy, _currentPage, PageSize);
|
||||
_products.AddRange(result.Products);
|
||||
_hasMore = result.HasNext;
|
||||
|
||||
SyncUrl();
|
||||
|
||||
_loadingMore = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private string GetSortByValue()
|
||||
{
|
||||
return _sortOption switch
|
||||
{
|
||||
ProductSortOption.PriceDesc => "price desc",
|
||||
ProductSortOption.PriceAsc => "price asc",
|
||||
ProductSortOption.Newest => "id desc",
|
||||
ProductSortOption.Title => "title asc",
|
||||
_ => "price desc"
|
||||
};
|
||||
}
|
||||
private async Task OnSortChanged() => await ReloadFromFilters();
|
||||
|
||||
private async Task OnSortChanged()
|
||||
{
|
||||
await LoadInitial();
|
||||
}
|
||||
|
||||
private async Task OnQueryChanged(KeyboardEventArgs _)
|
||||
{
|
||||
await LoadInitial();
|
||||
}
|
||||
private async Task OnQueryChanged(KeyboardEventArgs _) => await ReloadFromFilters();
|
||||
|
||||
private async Task AddToCart(Product p)
|
||||
{
|
||||
await GuestGate.RunAsync(() => Cart.Add(p, 1));
|
||||
}
|
||||
|
||||
private async Task OpenProduct(long productId)
|
||||
{
|
||||
await ShopListScrollRestore.SaveAsync(Js, ShopListScrollRestore.StoreKey, productId);
|
||||
Navigation.NavigateTo($"{RouteConstants.Store.ProductDetail}{productId}");
|
||||
}
|
||||
|
||||
private string FormatPrice(long price) => $"{VAT.AddVAT(price):N0} تومان";
|
||||
|
||||
public void Dispose()
|
||||
@@ -128,24 +174,33 @@ public partial class Products : ComponentBase, IDisposable
|
||||
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||
{
|
||||
_ = InvokeAsync(LoadInitial);
|
||||
}
|
||||
|
||||
private void UpdateCategoryFilterFromUri()
|
||||
{
|
||||
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
|
||||
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("category", out var values) &&
|
||||
long.TryParse(values.FirstOrDefault(), out var categoryId))
|
||||
if (_ignoreNextLocationChange)
|
||||
{
|
||||
_activeCategoryId = categoryId;
|
||||
_ignoreNextLocationChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_activeCategoryId = null;
|
||||
var uri = Navigation.ToAbsoluteUri(args.Location);
|
||||
if (!uri.AbsolutePath.Equals(RouteConstants.Store.Products, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
var incoming = ShopListQueryState.Parse(uri);
|
||||
if (incoming.Matches(CaptureState()))
|
||||
return;
|
||||
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
ApplyStateFromUri();
|
||||
await LoadPages(_currentPage);
|
||||
_pendingScrollRestore = true;
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void ClearCategoryFilter()
|
||||
private async Task ClearCategoryFilter()
|
||||
{
|
||||
Navigation.NavigateTo(RouteConstants.Store.Products);
|
||||
_activeCategoryId = null;
|
||||
_activeCategoryTitle = null;
|
||||
await ReloadFromFilters();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user