231da2cbaa
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>
213 lines
7.0 KiB
C#
213 lines
7.0 KiB
C#
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
|
|
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
|
|
namespace FrontOffice.Main.Utilities;
|
|
|
|
// ── DTOs ──
|
|
public record DiscountProductCard(
|
|
long Id,
|
|
string Title,
|
|
string ShortInfo,
|
|
long Price,
|
|
int MaxDiscountPercent,
|
|
string ImageUrl,
|
|
string ThumbnailUrl,
|
|
int RemainingCount,
|
|
int ViewCount,
|
|
bool IsActive,
|
|
DateTime Created);
|
|
|
|
public record DiscountProductDetail(
|
|
long Id,
|
|
string Title,
|
|
string ShortInfo,
|
|
string FullInfo,
|
|
long Price,
|
|
int MaxDiscountPercent,
|
|
string ImageUrl,
|
|
string ThumbnailUrl,
|
|
int RemainingCount,
|
|
int ViewCount,
|
|
int SortOrder,
|
|
bool IsActive,
|
|
List<DiscountCategoryInfo> Categories,
|
|
List<DiscountProductImageDto> Images,
|
|
DateTime Created);
|
|
|
|
public record DiscountCategoryInfo(long Id, string Name, string Title);
|
|
|
|
public record DiscountProductImageDto(
|
|
long Id,
|
|
string ImageUrl,
|
|
string ThumbnailUrl,
|
|
string? Title,
|
|
string? AltText,
|
|
int SortOrder);
|
|
|
|
public record DiscountCategoryNode(
|
|
long Id,
|
|
string Name,
|
|
string Title,
|
|
string? Description,
|
|
string? ImagePath,
|
|
long? ParentCategoryId,
|
|
int SortOrder,
|
|
bool IsActive,
|
|
int ProductCount,
|
|
List<DiscountCategoryNode> Children);
|
|
|
|
public record DiscountProductListResult(
|
|
List<DiscountProductCard> Products,
|
|
int TotalCount,
|
|
int TotalPages,
|
|
int CurrentPage);
|
|
|
|
// ── Service ──
|
|
public class DiscountProductService
|
|
{
|
|
private readonly DiscountProductContract.DiscountProductContractClient _productClient;
|
|
private readonly DiscountCategoryContract.DiscountCategoryContractClient _categoryClient;
|
|
|
|
public DiscountProductService(
|
|
DiscountProductContract.DiscountProductContractClient productClient,
|
|
DiscountCategoryContract.DiscountCategoryContractClient categoryClient)
|
|
{
|
|
_productClient = productClient;
|
|
_categoryClient = categoryClient;
|
|
}
|
|
|
|
public async Task<DiscountProductListResult> GetProductsAsync(
|
|
int page = 1, int pageSize = 12,
|
|
string? search = null, long? categoryId = null,
|
|
bool? inStock = null, string? sortBy = null)
|
|
{
|
|
try
|
|
{
|
|
var request = new GetDiscountProductsRequest
|
|
{
|
|
PageNumber = page,
|
|
PageSize = pageSize,
|
|
IsActive = true // فقط محصولات فعال
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
request.SearchQuery = search;
|
|
if (categoryId.HasValue)
|
|
request.CategoryId = categoryId.Value;
|
|
if (inStock.HasValue)
|
|
request.InStock = inStock.Value;
|
|
if (!string.IsNullOrWhiteSpace(sortBy))
|
|
request.SortBy = sortBy;
|
|
|
|
var response = await _productClient.GetDiscountProductsAsync(request);
|
|
|
|
var products = response.Models.Select(m => new DiscountProductCard(
|
|
Id: m.Id,
|
|
Title: m.Title ?? string.Empty,
|
|
ShortInfo: m.ShortInfomation ?? string.Empty,
|
|
Price: m.Price,
|
|
MaxDiscountPercent: m.MaxDiscountPercent,
|
|
ImageUrl: BuildUrl(m.ImagePath),
|
|
ThumbnailUrl: BuildUrl(m.ThumbnailPath),
|
|
RemainingCount: m.RemainingCount,
|
|
ViewCount: m.ViewCount,
|
|
IsActive: m.IsActive,
|
|
Created: m.Created?.ToDateTime() ?? DateTime.MinValue
|
|
)).ToList();
|
|
|
|
var totalCount = (int)(response.MetaData?.TotalCount ?? 0);
|
|
var totalPages = (int)(response.MetaData?.TotalPage ?? 0);
|
|
|
|
return new DiscountProductListResult(products, totalCount, totalPages, page);
|
|
}
|
|
catch
|
|
{
|
|
return new DiscountProductListResult(new(), 0, 0, page);
|
|
}
|
|
}
|
|
|
|
public Task<DiscountProductListResult> GetTopSellingAsync(int count = 6)
|
|
=> GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc");
|
|
|
|
public async Task<DiscountProductDetail?> GetByIdAsync(long productId)
|
|
{
|
|
try
|
|
{
|
|
var response = await _productClient.GetDiscountProductByIdAsync(
|
|
new GetDiscountProductByIdRequest { ProductId = productId });
|
|
|
|
var categories = response.Categories
|
|
.Select(c => new DiscountCategoryInfo(c.Id, c.Name ?? "", c.Title ?? ""))
|
|
.ToList();
|
|
|
|
// Load images
|
|
var imagesResponse = await _productClient.GetDiscountProductImagesAsync(
|
|
new GetDiscountProductImagesRequest { DiscountProductId = productId, OnlyActive = true });
|
|
|
|
var images = imagesResponse.Images
|
|
.OrderBy(i => i.SortOrder)
|
|
.Select(i => new DiscountProductImageDto(
|
|
Id: i.Id,
|
|
ImageUrl: BuildUrl(i.ImagePath),
|
|
ThumbnailUrl: BuildUrl(i.ThumbnailPath),
|
|
Title: i.Title,
|
|
AltText: i.AltText,
|
|
SortOrder: i.SortOrder))
|
|
.ToList();
|
|
|
|
return new DiscountProductDetail(
|
|
Id: response.Id,
|
|
Title: response.Title ?? string.Empty,
|
|
ShortInfo: response.ShortInfomation ?? string.Empty,
|
|
FullInfo: response.FullInformation ?? string.Empty,
|
|
Price: response.Price,
|
|
MaxDiscountPercent: response.MaxDiscountPercent,
|
|
ImageUrl: BuildUrl(response.ImagePath),
|
|
ThumbnailUrl: BuildUrl(response.ThumbnailPath),
|
|
RemainingCount: response.RemainingCount,
|
|
ViewCount: response.ViewCount,
|
|
SortOrder: response.SortOrder,
|
|
IsActive: response.IsActive,
|
|
Categories: categories,
|
|
Images: images,
|
|
Created: response.Created?.ToDateTime() ?? DateTime.MinValue);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<List<DiscountCategoryNode>> GetCategoriesAsync(bool onlyActive = true)
|
|
{
|
|
try
|
|
{
|
|
var request = new GetDiscountCategoriesRequest();
|
|
if (onlyActive) request.IsActive = true;
|
|
|
|
var response = await _categoryClient.GetDiscountCategoriesAsync(request);
|
|
return response.Categories.Select(MapCategory).ToList();
|
|
}
|
|
catch
|
|
{
|
|
return new();
|
|
}
|
|
}
|
|
|
|
private static DiscountCategoryNode MapCategory(DiscountCategoryDto dto) => new(
|
|
Id: dto.Id,
|
|
Name: dto.Name ?? "",
|
|
Title: dto.Title ?? "",
|
|
Description: dto.Description,
|
|
ImagePath: dto.ImagePath,
|
|
ParentCategoryId: dto.ParentCategoryId,
|
|
SortOrder: dto.SortOrder,
|
|
IsActive: dto.IsActive,
|
|
ProductCount: dto.ProductCount,
|
|
Children: dto.Children.Select(MapCategory).ToList());
|
|
|
|
private static string BuildUrl(string? path)
|
|
=> path ?? string.Empty;
|
|
}
|