Route categories to /products/category/{id}/{slug}, improve menu hover/layout, and hide stock/category clutter on product cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
import { meshkeeGet, tenantPath } from "@/lib/meshkee";
|
|
import { detailPath, slugifyForUrl } from "@/lib/seo-slug";
|
|
|
|
export type ApiCategory = {
|
|
id: string;
|
|
parentId?: string | null;
|
|
name?: string | null;
|
|
nameFa?: string | null;
|
|
slug?: string | null;
|
|
description?: string | null;
|
|
sortOrder?: number | null;
|
|
isActive?: boolean;
|
|
};
|
|
|
|
export type NavCategory = {
|
|
id: string;
|
|
title: string;
|
|
href: string;
|
|
image?: string;
|
|
children: NavCategory[];
|
|
};
|
|
|
|
function titleOf(category: Pick<ApiCategory, "nameFa" | "name" | "id">) {
|
|
return category.nameFa?.trim() || category.name?.trim() || String(category.id);
|
|
}
|
|
|
|
/** Public category URL: /products/category/{id}/{nameFaSlug} */
|
|
export function categoryHref(category: Pick<ApiCategory, "id" | "nameFa" | "name">) {
|
|
return detailPath("/products/category", category.id, titleOf(category));
|
|
}
|
|
|
|
export function categoryNameSlug(title: string) {
|
|
return slugifyForUrl(title);
|
|
}
|
|
|
|
export async function fetchProductCategories() {
|
|
const data = await meshkeeGet<{ items?: ApiCategory[] }>(tenantPath("/categories?entityType=product"), {
|
|
items: [],
|
|
});
|
|
return (data.items ?? []).filter((category) => category.isActive !== false);
|
|
}
|
|
|
|
export async function fetchCategoryById(categoryId: string) {
|
|
const data = await meshkeeGet<{ category?: ApiCategory }>(
|
|
tenantPath(`/categories/by-id/${encodeURIComponent(categoryId)}`),
|
|
{},
|
|
);
|
|
return data.category ?? null;
|
|
}
|
|
|
|
export function buildCategoryTree(flat: ApiCategory[]): NavCategory[] {
|
|
const byId = new Map(flat.map((category) => [String(category.id), category]));
|
|
const children = new Map<string, ApiCategory[]>();
|
|
|
|
for (const category of flat) {
|
|
const parentId = category.parentId ? String(category.parentId) : "";
|
|
const key = parentId && byId.has(parentId) ? parentId : "root";
|
|
const list = children.get(key) ?? [];
|
|
list.push(category);
|
|
children.set(key, list);
|
|
}
|
|
|
|
const sort = (list: ApiCategory[]) =>
|
|
[...list].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || titleOf(a).localeCompare(titleOf(b), "fa"));
|
|
|
|
const build = (category: ApiCategory): NavCategory => ({
|
|
id: String(category.id),
|
|
title: titleOf(category),
|
|
href: categoryHref(category),
|
|
children: sort(children.get(String(category.id)) ?? []).map(build),
|
|
});
|
|
|
|
return sort(children.get("root") ?? []).map(build);
|
|
}
|
|
|
|
/** Skip single-child wrapper roots (e.g. کالاها → لوازم…) so mega menu starts at useful columns. */
|
|
export function unwrapNavRoots(nodes: NavCategory[]): NavCategory[] {
|
|
let current = nodes;
|
|
while (current.length === 1 && current[0].children.length > 0) {
|
|
current = current[0].children;
|
|
}
|
|
return current;
|
|
}
|
|
|
|
export function flattenNavCategories(nodes: NavCategory[]): NavCategory[] {
|
|
const result: NavCategory[] = [];
|
|
const walk = (list: NavCategory[]) => {
|
|
for (const node of list) {
|
|
result.push(node);
|
|
if (node.children.length) walk(node.children);
|
|
}
|
|
};
|
|
walk(nodes);
|
|
return result;
|
|
}
|
|
|
|
export function findNavCategory(nodes: NavCategory[], id?: string | null) {
|
|
if (!id) return undefined;
|
|
return flattenNavCategories(nodes).find((item) => item.id === id);
|
|
}
|
|
|
|
/**
|
|
* Flatten one level for the mega menu: nested groups become sibling sections
|
|
* so CSS columns can pack them evenly.
|
|
*/
|
|
export function toMegaSections(categories: NavCategory[]): NavCategory[] {
|
|
const sections: NavCategory[] = [];
|
|
|
|
for (const category of categories) {
|
|
const leaves = category.children.filter((child) => child.children.length === 0);
|
|
const branches = category.children.filter((child) => child.children.length > 0);
|
|
|
|
if (!branches.length) {
|
|
sections.push(category);
|
|
continue;
|
|
}
|
|
|
|
if (leaves.length) {
|
|
sections.push({ ...category, children: leaves });
|
|
}
|
|
|
|
for (const branch of branches) {
|
|
sections.push(branch);
|
|
}
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
|
|
export async function getNavCategories() {
|
|
return unwrapNavRoots(buildCategoryTree(await fetchProductCategories()));
|
|
}
|