feat: enhance product listing and navigation functionality
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Added a wrapper div for product items in the DiscountStore and Store pages to improve layout consistency. - Implemented state management for URL synchronization and scroll restoration in both product components. - Refactored product loading logic to streamline data retrieval and improve user experience when navigating between product pages. These changes enhance the overall functionality and user experience of the product listing pages, ensuring better state management and navigation.
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user