setMegaOpen(true)}
onMouseLeave={() => setMegaOpen(false)}
>
-
+
محصولات
@@ -52,7 +52,11 @@ export function Header({ categories = [] }: { categories?: NavCategory[] }) {
{navLinks
.filter((link) => link.href !== "/" && link.href !== "/products")
.map((link) => (
-
+
{link.label}
))}
@@ -107,14 +111,25 @@ export function Header({ categories = [] }: { categories?: NavCategory[] }) {
{category.title}
{category.children.map((child) => (
-
setOpen(false)}
- >
- {child.title}
-
+
+ setOpen(false)}
+ >
+ {child.title}
+
+ {child.children.map((leaf) => (
+ setOpen(false)}
+ >
+ {leaf.title}
+
+ ))}
+
))}
))
diff --git a/components/layout/MegaMenu.tsx b/components/layout/MegaMenu.tsx
index 3779031..b3e19f5 100644
--- a/components/layout/MegaMenu.tsx
+++ b/components/layout/MegaMenu.tsx
@@ -1,44 +1,56 @@
"use client";
import Link from "next/link";
-import { Placeholder } from "@/components/Placeholder";
-import type { NavCategory } from "@/lib/categories";
+import { toMegaSections, type NavCategory } from "@/lib/categories";
+
+function CategoryLinks({ items, depth = 0 }: { items: NavCategory[]; depth?: number }) {
+ if (!items.length) return null;
+
+ return (
+
- {categories.length === 0 ? (
-
دستهبندیها بهزودی اضافه میشوند.
- ) : (
-
- {categories.map((category) => (
-
-
-
-
-
- {category.title}
-
- {category.children.length ? (
-
- {category.children.map((child) => (
- -
-
- {child.title}
-
-
- ))}
-
- ) : null}
-
- ))}
-
- )}
+
+
+ {sections.length === 0 ? (
+
دستهبندیها بهزودی اضافه میشوند.
+ ) : (
+
+ {sections.map((category) => (
+
+
+ {category.title}
+
+
+
+ ))}
+
+ )}
+
);
}
diff --git a/components/products/ProductsListing.tsx b/components/products/ProductsListing.tsx
new file mode 100644
index 0000000..74ca568
--- /dev/null
+++ b/components/products/ProductsListing.tsx
@@ -0,0 +1,54 @@
+import Link from "next/link";
+import { ProductCard } from "@/components/ui/ProductCard";
+import { faDigits } from "@/lib/format";
+import type { Product } from "@/lib/data";
+
+export function ProductsListing({
+ products,
+ total,
+ page,
+ pageCount,
+ pageHref,
+}: {
+ products: Product[];
+ total: number;
+ page: number;
+ pageCount: number;
+ pageHref: (page: number) => string;
+}) {
+ return (
+ <>
+ {total ?
{faDigits(total)} کالا
: null}
+
+ {products.length === 0 ? (
+
+
محصولی برای نمایش وجود ندارد
+
بهزودی موجودی فروشگاه از پنل Meshkee اینجا دیده میشود.
+
+ ) : (
+
+ {products.map((product) => (
+
+ ))}
+
+ )}
+
+ {pageCount > 1 ? (
+
+ {Array.from({ length: pageCount }, (_, index) => {
+ const number = index + 1;
+ return (
+
+ {faDigits(number)}
+
+ );
+ })}
+
+ ) : null}
+ >
+ );
+}
diff --git a/lib/categories.ts b/lib/categories.ts
index 80ebb33..3e62325 100644
--- a/lib/categories.ts
+++ b/lib/categories.ts
@@ -1,4 +1,5 @@
import { meshkeeGet, tenantPath } from "@/lib/meshkee";
+import { detailPath, slugifyForUrl } from "@/lib/seo-slug";
export type ApiCategory = {
id: string;
@@ -6,6 +7,7 @@ export type ApiCategory = {
name?: string | null;
nameFa?: string | null;
slug?: string | null;
+ description?: string | null;
sortOrder?: number | null;
isActive?: boolean;
};
@@ -15,15 +17,20 @@ export type NavCategory = {
title: string;
href: string;
image?: string;
- children: { id: string; title: string; href: string }[];
+ children: NavCategory[];
};
-function titleOf(category: ApiCategory) {
- return category.nameFa?.trim() || category.name?.trim() || "دستهبندی";
+function titleOf(category: Pick
) {
+ return category.nameFa?.trim() || category.name?.trim() || String(category.id);
}
-function hrefOf(category: ApiCategory) {
- return `/products?categoryId=${encodeURIComponent(category.id)}`;
+/** Public category URL: /products/category/{id}/{nameFaSlug} */
+export function categoryHref(category: Pick) {
+ return detailPath("/products/category", category.id, titleOf(category));
+}
+
+export function categoryNameSlug(title: string) {
+ return slugifyForUrl(title);
}
export async function fetchProductCategories() {
@@ -33,6 +40,14 @@ export async function fetchProductCategories() {
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();
@@ -48,18 +63,70 @@ export function buildCategoryTree(flat: ApiCategory[]): NavCategory[] {
const sort = (list: ApiCategory[]) =>
[...list].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0) || titleOf(a).localeCompare(titleOf(b), "fa"));
- return sort(children.get("root") ?? []).map((root) => ({
- id: String(root.id),
- title: titleOf(root),
- href: hrefOf(root),
- children: sort(children.get(String(root.id)) ?? []).map((child) => ({
- id: String(child.id),
- title: titleOf(child),
- href: hrefOf(child),
- })),
- }));
+ 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 buildCategoryTree(await fetchProductCategories());
+ return unwrapNavRoots(buildCategoryTree(await fetchProductCategories()));
}
diff --git a/lib/products-list.ts b/lib/products-list.ts
new file mode 100644
index 0000000..b72d3ad
--- /dev/null
+++ b/lib/products-list.ts
@@ -0,0 +1,41 @@
+import type { Product } from "@/lib/data";
+import {
+ catalogProductToCard,
+ fetchCatalogProducts,
+ fetchStoreItems,
+ variantToProduct,
+} from "@/lib/store";
+
+export async function loadCategoryProducts(params: {
+ page?: number;
+ pageSize?: number;
+ categoryId?: string;
+ name?: string;
+}) {
+ const page = params.page ?? 1;
+ const pageSize = params.pageSize ?? 20;
+ const [storeResult, catalogResult] = await Promise.all([
+ fetchStoreItems({
+ page,
+ pageSize,
+ categoryId: params.categoryId,
+ name: params.name,
+ }),
+ fetchCatalogProducts({
+ page,
+ pageSize,
+ categoryId: params.categoryId,
+ name: params.name,
+ }),
+ ]);
+
+ const fromStore = storeResult.items.map(variantToProduct);
+ const products: Product[] = fromStore.length
+ ? fromStore
+ : catalogResult.items.map(catalogProductToCard);
+ const total = fromStore.length ? storeResult.total : catalogResult.total;
+ const resolvedPageSize = fromStore.length ? storeResult.pageSize : catalogResult.pageSize;
+ const pageCount = Math.max(1, Math.ceil((total || products.length) / (resolvedPageSize || pageSize)));
+
+ return { products, total, pageCount, pageSize: resolvedPageSize || pageSize };
+}
diff --git a/lib/store.ts b/lib/store.ts
index 52ad061..2599bb1 100644
--- a/lib/store.ts
+++ b/lib/store.ts
@@ -255,7 +255,7 @@ export function catalogProductToCard(product: CatalogProduct): Product {
id: String(product.id),
href: productHref(product),
title: catalogTitle(product),
- brand: brandName(product) || product.categoryNameFa || "",
+ brand: brandName(product),
price: price ?? undefined,
image: product.thumbnailUrl || product.image || product.thumbnail || PLACEHOLDER,
inStock: product.store?.inStock !== false,