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> 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.Success(model); } catch (ValidationException exception) { var problem = ResultProblem.FromValidation(exception); return Result.Failure(problem); } catch (Exception exception) { var problem = ResultProblem.Unexpected("Failed to load navigation menu.", exception); return Result.Failure(problem); } } private static IReadOnlyList BuildColumns(IReadOnlyList? roots) { if (roots is null || roots.Count == 0) { return []; } var columns = new List(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(); } }