207 lines
7.5 KiB
C#
207 lines
7.5 KiB
C#
using System.Net;
|
|
using FluentValidation;
|
|
using Prefab.Domain.Exceptions;
|
|
using Prefab.Endpoints;
|
|
using Prefab.Shared.Catalog;
|
|
using Prefab.Shared.Catalog.Products;
|
|
using Prefab.Web.Client.Models.Catalog;
|
|
using Prefab.Web.Client.Models.Home;
|
|
using Prefab.Web.Client.Models.Shared;
|
|
using Prefab.Web.Client.Services;
|
|
using Prefab.Web.Client.ViewModels.Catalog;
|
|
using UrlBuilder = Prefab.Web.UrlPolicy.UrlPolicy;
|
|
|
|
namespace Prefab.Web.Gateway;
|
|
|
|
public static class Home
|
|
{
|
|
public sealed class Endpoints : IEndpointRegistrar
|
|
{
|
|
public void MapEndpoints(IEndpointRouteBuilder endpoints)
|
|
{
|
|
endpoints.MapGet(
|
|
"/bff/home",
|
|
async (Service service, CancellationToken cancellationToken) =>
|
|
{
|
|
var result = await service.Get(cancellationToken);
|
|
var status = result.Problem?.StatusCode ?? (result.IsSuccess
|
|
? StatusCodes.Status200OK
|
|
: StatusCodes.Status500InternalServerError);
|
|
return Results.Json(result, statusCode: status);
|
|
})
|
|
.WithTags(nameof(Home));
|
|
}
|
|
}
|
|
|
|
public sealed class Service(IModuleClient moduleClient, IProductListingService listingService) : IHomePageService
|
|
{
|
|
private const int MaxCategories = 8;
|
|
private const int MaxFeaturedProducts = 8;
|
|
|
|
public async Task<Result<HomePageModel>> Get(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var response = await moduleClient.Product.GetCategoryTree(depth: 3, cancellationToken);
|
|
var nodes = response.Categories ?? [];
|
|
|
|
var leafNodes = Flatten(nodes)
|
|
.Where(node => node.IsLeaf && !string.IsNullOrWhiteSpace(node.Slug))
|
|
.ToList();
|
|
|
|
var categories = new List<CategoryCardModel>(MaxCategories);
|
|
var featuredProducts = new List<ProductCardModel>(MaxFeaturedProducts);
|
|
var seenCategoryIds = new HashSet<Guid>();
|
|
var seenProductKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
await CollectAsync(
|
|
OrderCandidates(leafNodes.Where(node => node.IsFeatured)),
|
|
categories,
|
|
featuredProducts,
|
|
seenCategoryIds,
|
|
seenProductKeys,
|
|
cancellationToken);
|
|
|
|
if (categories.Count < MaxCategories || featuredProducts.Count < MaxFeaturedProducts)
|
|
{
|
|
await CollectAsync(
|
|
OrderCandidates(leafNodes.Where(node => !node.IsFeatured)),
|
|
categories,
|
|
featuredProducts,
|
|
seenCategoryIds,
|
|
seenProductKeys,
|
|
cancellationToken);
|
|
}
|
|
|
|
var model = new HomePageModel
|
|
{
|
|
LatestCategories = categories,
|
|
FeaturedProducts = featuredProducts
|
|
};
|
|
|
|
return Result<HomePageModel>.Success(model);
|
|
}
|
|
catch (ValidationException validationException)
|
|
{
|
|
var problem = ResultProblem.FromValidation(validationException);
|
|
return Result<HomePageModel>.Failure(problem);
|
|
}
|
|
catch (DomainException domainException)
|
|
{
|
|
var problem = ResultProblem.Unexpected(domainException.Message, domainException, HttpStatusCode.UnprocessableEntity);
|
|
return Result<HomePageModel>.Failure(problem);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
var problem = ResultProblem.Unexpected("Failed to retrieve home page content.", exception);
|
|
return Result<HomePageModel>.Failure(problem);
|
|
}
|
|
}
|
|
|
|
private async Task CollectAsync(
|
|
IEnumerable<GetCategoryModels.CategoryNodeDto> candidates,
|
|
ICollection<CategoryCardModel> categories,
|
|
ICollection<ProductCardModel> featuredProducts,
|
|
ISet<Guid> seenCategoryIds,
|
|
ISet<string> seenProductKeys,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
foreach (var candidate in candidates)
|
|
{
|
|
if (categories.Count >= MaxCategories && featuredProducts.Count >= MaxFeaturedProducts)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Result<ProductListingModel>? listingResult = null;
|
|
try
|
|
{
|
|
listingResult = await listingService.GetCategoryProducts(candidate.Slug, cancellationToken);
|
|
}
|
|
catch (DomainException)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!listingResult.IsSuccess || listingResult.Value is not { Products.Count: > 0 } listing)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (categories.Count < MaxCategories && seenCategoryIds.Add(candidate.Id))
|
|
{
|
|
categories.Add(new CategoryCardModel
|
|
{
|
|
Title = candidate.Name,
|
|
Url = UrlBuilder.BrowseProducts(candidate.Slug),
|
|
ImageUrl = string.IsNullOrWhiteSpace(candidate.HeroImageUrl) ? null : candidate.HeroImageUrl,
|
|
SecondaryText = FormatProductCount(listing.Total)
|
|
});
|
|
}
|
|
|
|
if (featuredProducts.Count >= MaxFeaturedProducts)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var product in listing.Products)
|
|
{
|
|
var key = GetProductKey(product);
|
|
if (!seenProductKeys.Add(key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
featuredProducts.Add(product);
|
|
|
|
if (featuredProducts.Count >= MaxFeaturedProducts)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetProductKey(ProductCardModel product)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(product.Sku))
|
|
{
|
|
return product.Sku!;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(product.Url))
|
|
{
|
|
return product.Url!;
|
|
}
|
|
|
|
return product.Title;
|
|
}
|
|
|
|
private static IEnumerable<GetCategoryModels.CategoryNodeDto> OrderCandidates(
|
|
IEnumerable<GetCategoryModels.CategoryNodeDto> candidates) =>
|
|
candidates
|
|
.OrderBy(candidate => candidate.DisplayOrder)
|
|
.ThenBy(candidate => candidate.Name, StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static string FormatProductCount(int total) =>
|
|
total == 1 ? "1 Product" : $"{total} Products";
|
|
|
|
private static IEnumerable<GetCategoryModels.CategoryNodeDto> Flatten(
|
|
IEnumerable<GetCategoryModels.CategoryNodeDto> nodes)
|
|
{
|
|
foreach (var node in nodes)
|
|
{
|
|
yield return node;
|
|
|
|
if (node.Children is { Count: > 0 })
|
|
{
|
|
foreach (var child in Flatten(node.Children))
|
|
{
|
|
yield return child;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|