Files
2025-10-27 17:39:18 -04:00

130 lines
4.5 KiB
C#

using FluentValidation;
using Prefab.Endpoints;
using Prefab.Shared.Catalog;
using Prefab.Shared.Catalog.Products;
using Prefab.Web.Client.Models.Shared;
using Prefab.Web.Client.Services;
using UrlBuilder = Prefab.Web.UrlPolicy.UrlPolicy;
namespace Prefab.Web.Gateway.Shared;
public static class NavMenu
{
public class Endpoints : IEndpointRegistrar
{
public void MapEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapGet("/bff/nav-menu/{depth:int}", async (INavMenuService service, int depth, CancellationToken cancellationToken) =>
{
var result = await service.Get(depth, cancellationToken);
var status = result.Problem?.StatusCode ?? (result.IsSuccess
? StatusCodes.Status200OK
: StatusCodes.Status500InternalServerError);
return Results.Json(result, statusCode: status);
})
.WithName(nameof(NavMenu));
}
}
public class Service(IModuleClient moduleClient) : INavMenuService
{
private const int MaxItemsPerColumn = 8;
public async Task<Result<NavMenuModel>> Get(int depth, CancellationToken cancellationToken)
{
var normalizedDepth = Math.Clamp(depth, 1, 2);
try
{
var response = await moduleClient.Product.GetCategoryTree(normalizedDepth, cancellationToken);
var columns = BuildColumns(response.Categories);
var model = new NavMenuModel(normalizedDepth, MaxItemsPerColumn, columns);
return Result<NavMenuModel>.Success(model);
}
catch (ValidationException exception)
{
var problem = ResultProblem.FromValidation(exception);
return Result<NavMenuModel>.Failure(problem);
}
catch (Exception exception)
{
var problem = ResultProblem.Unexpected("Failed to load navigation menu.", exception);
return Result<NavMenuModel>.Failure(problem);
}
}
private static IReadOnlyList<NavMenuColumnModel> BuildColumns(IReadOnlyList<GetCategoryModels.CategoryNodeDto>? roots)
{
if (roots is null || roots.Count == 0)
{
return [];
}
var columns = new List<NavMenuColumnModel>(roots.Count);
columns.AddRange(roots.Select(BuildColumn));
return columns;
}
private static NavMenuColumnModel BuildColumn(GetCategoryModels.CategoryNodeDto root)
{
var rootSlug = NormalizeSlug(root.Slug);
var isRootLeaf = root.IsLeaf;
var rootUrl = isRootLeaf
? UrlBuilder.BrowseProducts(rootSlug)
: UrlBuilder.BrowseCategory(rootSlug);
var rootLink = new NavMenuLinkModel(
root.Id.ToString(),
root.Name,
rootSlug,
isRootLeaf,
rootUrl);
var children = (root.Children ?? [])
.OrderBy(child => child.DisplayOrder)
.ThenBy(child => child.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
var featured = children
.Where(child => child.IsFeatured)
.ToList();
var menuCandidates = featured.Count > 0 ? featured : children;
var items = menuCandidates
.Take(MaxItemsPerColumn)
.Select(ToLink)
.ToList();
var hasMore = menuCandidates.Count > items.Count;
var seeAllUrl = isRootLeaf
? UrlBuilder.BrowseProducts(rootSlug)
: UrlBuilder.BrowseCategory(rootSlug);
return new NavMenuColumnModel(rootLink, items, hasMore, seeAllUrl);
}
private static NavMenuLinkModel ToLink(GetCategoryModels.CategoryNodeDto node)
{
var slug = NormalizeSlug(node.Slug);
var isLeaf = node.IsLeaf;
var url = isLeaf
? UrlBuilder.BrowseProducts(slug)
: UrlBuilder.BrowseCategory(slug);
return new NavMenuLinkModel(
node.Id.ToString(),
node.Name,
slug,
isLeaf,
url);
}
private static string NormalizeSlug(string slug) =>
string.IsNullOrWhiteSpace(slug)
? string.Empty
: slug.Trim().ToLowerInvariant();
}
}