Add category browsing and product filtering with breadcrumbs

This commit is contained in:
masoodafar-web
2025-11-28 11:15:58 +03:30
parent fe5f7bd9b9
commit 6ab835f7e9
11 changed files with 305 additions and 13 deletions

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FrontOffice.BFF.Category.Protobuf.Protos.Category;
namespace FrontOffice.Main.Utilities;
public sealed record CategoryItem(long Id, string Title, long? ParentId, string? ImagePath, bool IsActive);
public class CategoryService
{
private readonly CategoryContract.CategoryContractClient _client;
private List<CategoryItem>? _cache;
public CategoryService(CategoryContract.CategoryContractClient client)
{
_client = client;
}
public async Task<List<CategoryItem>> GetAllAsync(bool forceReload = false)
{
if (!forceReload && _cache is { Count: > 0 })
{
return _cache;
}
var response = await _client.GetAllCategoriesAsync(new GetCategoriesRequest());
_cache = response.Categories
.Select(dto => new CategoryItem(
Id: dto.Id,
Title: dto.Title ?? dto.Name ?? string.Empty,
ParentId: dto.ParentId,
ImagePath: dto.ImagePath,
IsActive: dto.IsActive))
.ToList();
return _cache;
}
public async Task<CategoryItem?> GetByIdAsync(long id)
{
var categories = await GetAllAsync();
return categories.FirstOrDefault(c => c.Id == id);
}
}

View File

@@ -1,7 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using FrontOffice.BFF.Products.Protobuf.Protos.Products;
using Google.Protobuf.WellKnownTypes;
namespace FrontOffice.Main.Utilities;
@@ -46,7 +49,8 @@ public record ProductCategoryPathInfo(
public class ProductService
{
private readonly ConcurrentDictionary<long, Product> _cache = new();
private readonly ConcurrentDictionary<long, CacheEntry> _cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(1);
private readonly ProductsContract.ProductsContractClient _client;
public ProductService(ProductsContract.ProductsContractClient client)
@@ -54,11 +58,11 @@ public class ProductService
_client = client;
}
public async Task<List<Product>> GetProductsAsync(string? query = null)
public async Task<List<Product>> GetProductsAsync(string? query = null, long? categoryId = null)
{
try
{
var resp = await _client.GetAllProductsByFilterAsync(new GetAllProductsByFilterRequest
var request = new GetAllProductsByFilterRequest
{
Filter = new GetAllProductsByFilterFilter
{
@@ -67,12 +71,19 @@ public class ProductService
ShortInfomation = query ?? string.Empty,
FullInformation = query ?? string.Empty
}
});
};
if (categoryId is { } value)
{
request.Filter.CategoryId = value ;
}
var resp = await _client.GetAllProductsByFilterAsync(request);
return MapAndCache(resp.Models);
}
catch
{
IEnumerable<Product> list = _cache.Values;
IEnumerable<Product> list = GetValidCachedProducts();
if (!string.IsNullOrWhiteSpace(query))
{
var q = query.Trim();
@@ -87,7 +98,7 @@ public class ProductService
public async Task<Product?> GetByIdAsync(long id)
{
if (_cache.TryGetValue(id, out var cached)) return cached;
if (TryGetCachedProduct(id, out var cached)) return cached;
try
{
@@ -101,7 +112,7 @@ public class ProductService
}
catch
{
_cache.TryGetValue(id, out var result);
TryGetCachedProduct(id, out var result);
return result;
}
}
@@ -123,9 +134,7 @@ public class ProductService
Rate: m.Rate,
RemainingCount: m.RemainingCount
);
if (_cache.All(a => a.Key != p.Id))
_cache[p.Id] = p;
CacheProduct(p);
list.Add(p);
}
@@ -159,10 +168,42 @@ public class ProductService
Categories = MapCategoryPaths(model.Categories)
};
_cache[product.Id] = product;
CacheProduct(product);
return product;
}
private void CacheProduct(Product product)
{
var entry = new CacheEntry(product, DateTime.UtcNow.Add(CacheDuration));
_cache.AddOrUpdate(product.Id, entry, (_, _) => entry);
}
private bool TryGetCachedProduct(long id, [NotNullWhen(true)] out Product? product)
{
if (_cache.TryGetValue(id, out var entry))
{
if (entry.Expiration > DateTime.UtcNow)
{
product = entry.Product;
return true;
}
_cache.TryRemove(id, out _);
}
product = null;
return false;
}
private IEnumerable<Product> GetValidCachedProducts()
{
var now = DateTime.UtcNow;
return _cache.Values
.Where(entry => entry.Expiration > now)
.Select(entry => entry.Product);
}
private sealed record CacheEntry(Product Product, DateTime Expiration);
private static string BuildUrl(string? path) =>
string.IsNullOrWhiteSpace(path) ? string.Empty : UrlUtility.DownloadUrl + path;

View File

@@ -55,5 +55,6 @@ public static class RouteConstants
public const string CheckoutSummary = "/checkout-summary";
public const string Orders = "/orders";
public const string OrderDetail = "/order/"; // usage: /order/{id}
public const string Categories = "/categories";
}
}