Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m42s

This commit is contained in:
masoodafar-web
2026-02-18 01:19:34 +03:30
5 changed files with 150 additions and 34 deletions
@@ -64,7 +64,7 @@
<MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText> <MudText Class="mt-2 mud-text-secondary">در حال بارگذاری...</MudText>
</MudStack> </MudStack>
} }
else if (_result.Products.Count == 0) else if (_products.Count == 0)
{ {
<MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert> <MudAlert Severity="Severity.Info" Class="my-4">محصولی یافت نشد.</MudAlert>
} }
@@ -72,7 +72,7 @@
{ {
<!-- Products Grid --> <!-- Products Grid -->
<MudGrid Spacing="1"> <MudGrid Spacing="1">
@foreach (var p in _result.Products) @foreach (var p in _products)
{ {
<MudItem xs="6" sm="6" md="3" <MudItem xs="6" sm="6" md="3"
onclick="@(() => NavigateToProduct(p.Id))"> onclick="@(() => NavigateToProduct(p.Id))">
@@ -119,15 +119,29 @@
} }
</MudGrid> </MudGrid>
<!-- Pagination --> @* Lazy Load — بارگذاری بیشتر *@
@if (_result.TotalPages > 1) @if (_loadingMore)
{ {
<div class="d-flex justify-center mt-4"> <MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudPagination Count="@_result.TotalPages" Selected="@_currentPage" <MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
SelectedChanged="OnPageChanged" <MudText Typo="Typo.caption" Class="mud-text-secondary">بارگذاری بیشتر...</MudText>
Color="Color.Primary" Variant="Variant.Filled" </MudStack>
BoundaryCount="1" MiddleCount="3" /> }
else if (_hasMore)
{
<div class="d-flex justify-center py-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ExpandMore"
OnClick="LoadMore">
نمایش محصولات بیشتر
</MudButton>
</div> </div>
} }
else if (_products.Count > 0)
{
<MudText Typo="Typo.caption" Align="Align.Center" Class="py-4 mud-text-secondary">
همه @(_products.Count) محصول نمایش داده شد
</MudText>
}
} }
</MudContainer> </MudContainer>
@@ -13,9 +13,12 @@ public partial class Products : ComponentBase, IDisposable
private long? _selectedCategoryId; private long? _selectedCategoryId;
private int _currentPage = 1; private int _currentPage = 1;
private bool _loading; private bool _loading;
private bool _loadingMore;
private bool _hasMore = true;
private int _totalCount;
private const int PageSize = 12; private const int PageSize = 12;
private DiscountProductListResult _result = new(new(), 0, 0, 1); private List<DiscountProductCard> _products = new();
private List<DiscountCategoryNode> _categories = new(); private List<DiscountCategoryNode> _categories = new();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@@ -27,45 +30,66 @@ public partial class Products : ComponentBase, IDisposable
var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize); var productsTask = ProductService.GetProductsAsync(page: 1, pageSize: PageSize);
await Task.WhenAll(categoriesTask, productsTask); await Task.WhenAll(categoriesTask, productsTask);
_categories = categoriesTask.Result; _categories = categoriesTask.Result;
_result = productsTask.Result; var result = productsTask.Result;
_products = result.Products;
_totalCount = result.TotalCount;
_hasMore = result.CurrentPage < result.TotalPages;
_loading = false; _loading = false;
} }
private async Task LoadProductsAsync() private async Task LoadInitial()
{ {
_loading = true; _loading = true;
_currentPage = 1;
_products.Clear();
StateHasChanged(); StateHasChanged();
_result = await ProductService.GetProductsAsync(
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;
_loading = false;
StateHasChanged();
}
private async Task LoadMore()
{
if (_loadingMore || !_hasMore) return;
_loadingMore = true;
StateHasChanged();
_currentPage++;
var result = await ProductService.GetProductsAsync(
page: _currentPage, page: _currentPage,
pageSize: PageSize, pageSize: PageSize,
search: string.IsNullOrWhiteSpace(_search) ? null : _search, search: string.IsNullOrWhiteSpace(_search) ? null : _search,
categoryId: _selectedCategoryId); categoryId: _selectedCategoryId);
_loading = false; _products.AddRange(result.Products);
_hasMore = result.CurrentPage < result.TotalPages;
_loadingMore = false;
StateHasChanged(); StateHasChanged();
} }
private async Task SearchProducts() => await LoadProductsAsync(); private async Task SearchProducts() => await LoadInitial();
private async Task OnSearchKeyUp(KeyboardEventArgs e) private async Task OnSearchKeyUp(KeyboardEventArgs e)
{ {
if (e.Key == "Enter") if (e.Key == "Enter")
{ {
_currentPage = 1; await LoadInitial();
await LoadProductsAsync();
} }
} }
private async Task OnCategoryChanged(long? value) private async Task OnCategoryChanged(long? value)
{ {
_selectedCategoryId = value; _selectedCategoryId = value;
_currentPage = 1; await LoadInitial();
await LoadProductsAsync();
}
private async Task OnPageChanged(int page)
{
_currentPage = page;
await LoadProductsAsync();
} }
private async Task AddToCart(DiscountProductCard p) private async Task AddToCart(DiscountProductCard p)
@@ -94,7 +94,7 @@
{ {
<MudText Typo="Typo.body2" Class="mud-text-secondary"> <MudText Typo="Typo.body2" Class="mud-text-secondary">
<MudIcon Icon="@Icons.Material.Filled.Inventory2" Size="Size.Small" Class="me-1"/> <MudIcon Icon="@Icons.Material.Filled.Inventory2" Size="Size.Small" Class="me-1"/>
@(_products.Count) محصول @(_totalCount) محصول
</MudText> </MudText>
} }
</MudItem> </MudItem>
@@ -159,5 +159,30 @@
</MudItem> </MudItem>
} }
</MudGrid> </MudGrid>
@* Lazy Load — بارگذاری بیشتر *@
@if (_loadingMore)
{
<MudStack AlignItems="AlignItems.Center" Class="py-4">
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
<MudText Typo="Typo.caption" Class="mud-text-secondary">بارگذاری بیشتر...</MudText>
</MudStack>
}
else if (_hasMore)
{
<div class="d-flex justify-center py-4">
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.ExpandMore"
OnClick="LoadMore">
نمایش محصولات بیشتر
</MudButton>
</div>
}
else if (_products.Count > 0)
{
<MudText Typo="Typo.caption" Align="Align.Center" Class="py-4 mud-text-secondary">
همه @(_products.Count) محصول نمایش داده شد
</MudText>
}
} }
</MudContainer> </MudContainer>
@@ -24,6 +24,11 @@ public partial class Products : ComponentBase, IDisposable
private string _query = string.Empty; private string _query = string.Empty;
private bool _loading; private bool _loading;
private bool _loadingMore;
private bool _hasMore = true;
private int _currentPage = 1;
private int _totalCount;
private const int PageSize = 12;
private List<Product> _products = new(); private List<Product> _products = new();
private long? _activeCategoryId; private long? _activeCategoryId;
private string? _activeCategoryTitle; private string? _activeCategoryTitle;
@@ -36,7 +41,7 @@ public partial class Products : ComponentBase, IDisposable
await Cart.EnsureInitializedAsync(); await Cart.EnsureInitializedAsync();
Cart.OnChange += StateHasChanged; Cart.OnChange += StateHasChanged;
Navigation.LocationChanged += HandleLocationChanged; Navigation.LocationChanged += HandleLocationChanged;
await Load(); await LoadInitial();
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -49,18 +54,40 @@ public partial class Products : ComponentBase, IDisposable
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
private async Task Load() private async Task LoadInitial()
{ {
_loading = true; _loading = true;
_currentPage = 1;
_products.Clear();
UpdateCategoryFilterFromUri(); UpdateCategoryFilterFromUri();
var sortBy = GetSortByValue(); var sortBy = GetSortByValue();
_products = await ProductService.GetProductsAsync(_query, _activeCategoryId, sortBy); var result = await ProductService.GetProductsPagedAsync(_query, _activeCategoryId, sortBy, _currentPage, PageSize);
_products = result.Products;
_hasMore = result.HasNext;
_totalCount = result.TotalCount;
_activeCategoryTitle = _activeCategoryId is { } categoryId _activeCategoryTitle = _activeCategoryId is { } categoryId
? (await CategoryService.GetByIdAsync(categoryId))?.Title ? (await CategoryService.GetByIdAsync(categoryId))?.Title
: null; : null;
_loading = false; _loading = false;
} }
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);
_products.AddRange(result.Products);
_hasMore = result.HasNext;
_loadingMore = false;
StateHasChanged();
}
private string GetSortByValue() private string GetSortByValue()
{ {
return _sortOption switch return _sortOption switch
@@ -75,12 +102,12 @@ public partial class Products : ComponentBase, IDisposable
private async Task OnSortChanged() private async Task OnSortChanged()
{ {
await Load(); await LoadInitial();
} }
private async Task OnQueryChanged(KeyboardEventArgs _) private async Task OnQueryChanged(KeyboardEventArgs _)
{ {
await Load(); await LoadInitial();
} }
private async Task AddToCart(Product p) private async Task AddToCart(Product p)
@@ -98,7 +125,7 @@ public partial class Products : ComponentBase, IDisposable
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args) private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
{ {
_ = InvokeAsync(Load); _ = InvokeAsync(LoadInitial);
} }
private void UpdateCategoryFilterFromUri() private void UpdateCategoryFilterFromUri()
@@ -26,6 +26,13 @@ public record Product(
= []; = [];
} }
public record ProductListResult(
List<Product> Products,
int TotalCount,
int TotalPages,
int CurrentPage,
bool HasNext);
public record ProductGalleryImage( public record ProductGalleryImage(
long ProductGalleryId, long ProductGalleryId,
long ProductImageId, long ProductImageId,
@@ -59,11 +66,24 @@ public class ProductService
} }
public async Task<List<Product>> GetProductsAsync(string? query = null, long? categoryId = null, string? sortBy = null) public async Task<List<Product>> GetProductsAsync(string? query = null, long? categoryId = null, string? sortBy = null)
{
var result = await GetProductsPagedAsync(query: query, categoryId: categoryId, sortBy: sortBy, page: 1, pageSize: 500);
return result.Products;
}
public async Task<ProductListResult> GetProductsPagedAsync(
string? query = null, long? categoryId = null, string? sortBy = null,
int page = 1, int pageSize = 12)
{ {
try try
{ {
var request = new GetAllProductsByFilterRequest var request = new GetAllProductsByFilterRequest
{ {
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = page,
PageSize = pageSize
},
Filter = new GetAllProductsByFilterFilter Filter = new GetAllProductsByFilterFilter
{ {
Title = query ?? string.Empty, Title = query ?? string.Empty,
@@ -84,7 +104,12 @@ public class ProductService
} }
var resp = await _client.GetAllProductsByFilterAsync(request); var resp = await _client.GetAllProductsByFilterAsync(request);
return MapAndCache(resp.Models); var products = MapAndCache(resp.Models);
var totalCount = (int)(resp.MetaData?.TotalCount ?? 0);
var totalPages = (int)(resp.MetaData?.TotalPage ?? 0);
var hasNext = resp.MetaData?.HasNext ?? false;
return new ProductListResult(products, totalCount, totalPages, page, hasNext);
} }
catch catch
{ {
@@ -97,7 +122,8 @@ public class ProductService
p.Description.Contains(q, StringComparison.OrdinalIgnoreCase)); p.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
} }
return list.OrderBy(p => p.Id).ToList(); var all = list.OrderBy(p => p.Id).ToList();
return new ProductListResult(all, all.Count, 1, 1, false);
} }
} }