Add category detail URLs and polish mega menu UX.
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>
This commit is contained in:
co-authored by
Cursor
parent
5d53cf91eb
commit
f078cdad0e
@@ -32,17 +32,17 @@ export function Header({ categories = [] }: { categories?: NavCategory[] }) {
|
||||
<Placeholder src={site.logo} alt={site.name} className="h-14 w-auto object-contain" />
|
||||
</Link>
|
||||
|
||||
<nav className="mx-2 hidden items-center lg:flex">
|
||||
<Link href="/" className="px-3 py-2 text-[14px] hover:text-brand">
|
||||
<nav className="mx-2 hidden h-14 items-center lg:flex">
|
||||
<Link href="/" className="inline-flex h-full items-center px-3 text-[14px] hover:text-brand">
|
||||
خانه
|
||||
</Link>
|
||||
|
||||
<div
|
||||
className="relative"
|
||||
className="relative flex h-full items-center"
|
||||
onMouseEnter={() => setMegaOpen(true)}
|
||||
onMouseLeave={() => setMegaOpen(false)}
|
||||
>
|
||||
<Link href="/products" className="flex items-center gap-1 px-3 py-2 text-[14px] hover:text-brand">
|
||||
<Link href="/products" className="inline-flex h-full items-center gap-1 px-3 text-[14px] hover:text-brand">
|
||||
محصولات
|
||||
<ChevronDown className="size-3.5" />
|
||||
</Link>
|
||||
@@ -52,7 +52,11 @@ export function Header({ categories = [] }: { categories?: NavCategory[] }) {
|
||||
{navLinks
|
||||
.filter((link) => link.href !== "/" && link.href !== "/products")
|
||||
.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="px-3 py-2 text-[14px] hover:text-brand">
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="inline-flex h-full items-center px-3 text-[14px] hover:text-brand"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
@@ -107,14 +111,25 @@ export function Header({ categories = [] }: { categories?: NavCategory[] }) {
|
||||
{category.title}
|
||||
</Link>
|
||||
{category.children.map((child) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
href={child.href}
|
||||
className="block py-1 text-[12px] text-muted"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{child.title}
|
||||
</Link>
|
||||
<div key={child.id} className="ps-2">
|
||||
<Link
|
||||
href={child.href}
|
||||
className="block py-1 text-[12px] font-medium text-ink"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{child.title}
|
||||
</Link>
|
||||
{child.children.map((leaf) => (
|
||||
<Link
|
||||
key={leaf.id}
|
||||
href={leaf.href}
|
||||
className="block py-1 ps-3 text-[12px] text-muted"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{leaf.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -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 (
|
||||
<ul className={depth === 0 ? "space-y-1" : "mt-1 space-y-1 border-r border-line/70 pr-2 mr-1"}>
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`block rounded-lg px-2 py-1 transition hover:bg-paper hover:text-brand ${
|
||||
depth === 0 ? "text-[12.5px] text-muted" : "text-[12px] text-muted/90"
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
{item.children.length ? <CategoryLinks items={item.children} depth={depth + 1} /> : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function MegaMenu({ open, categories }: { open: boolean; categories: NavCategory[] }) {
|
||||
if (!open) return null;
|
||||
|
||||
const sections = toMegaSections(categories);
|
||||
|
||||
return (
|
||||
<div className="absolute top-full right-0 z-50 mt-1 w-[min(920px,calc(100vw-2rem))] border border-line bg-white p-5 shadow-[0_20px_50px_rgba(0,0,0,0.08)]">
|
||||
{categories.length === 0 ? (
|
||||
<p className="text-[13px] text-muted">دستهبندیها بهزودی اضافه میشوند.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-5 md:grid-cols-3 lg:grid-cols-4">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
<Link href={category.href} className="mb-3 flex items-center gap-2 text-[13.5px] font-semibold text-ink hover:text-brand">
|
||||
<span className="grid size-9 place-items-center overflow-hidden rounded-xl bg-paper">
|
||||
<Placeholder src={category.image || "/placeholders/category.svg"} className="size-full object-cover" />
|
||||
</span>
|
||||
{category.title}
|
||||
</Link>
|
||||
{category.children.length ? (
|
||||
<ul className="space-y-1.5 pr-1">
|
||||
{category.children.map((child) => (
|
||||
<li key={child.id}>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="block rounded-lg px-2 py-1 text-[12.5px] text-muted transition hover:bg-paper hover:text-brand"
|
||||
>
|
||||
{child.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-full right-0 z-50 -mt-1 pt-3">
|
||||
<div className="max-h-[min(70vh,640px)] w-[min(920px,calc(100vw-2rem))] overflow-y-auto border border-line bg-white p-5 shadow-[0_20px_50px_rgba(0,0,0,0.08)]">
|
||||
{sections.length === 0 ? (
|
||||
<p className="text-[13px] text-muted">دستهبندیها بهزودی اضافه میشوند.</p>
|
||||
) : (
|
||||
<div className="columns-2 gap-6 md:columns-3 lg:columns-4">
|
||||
{sections.map((category) => (
|
||||
<div key={category.id} className="mb-6 break-inside-avoid">
|
||||
<Link
|
||||
href={category.href}
|
||||
className="mb-2 block text-[13.5px] font-semibold text-ink hover:text-brand"
|
||||
>
|
||||
{category.title}
|
||||
</Link>
|
||||
<CategoryLinks items={category.children} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 ? <p className="mb-6 text-[13px] text-muted">{faDigits(total)} کالا</p> : null}
|
||||
|
||||
{products.length === 0 ? (
|
||||
<div className="rounded-2xl border border-line bg-white px-6 py-16 text-center">
|
||||
<p className="text-[15px] font-bold text-ink">محصولی برای نمایش وجود ندارد</p>
|
||||
<p className="mt-2 text-[13px] text-muted">بهزودی موجودی فروشگاه از پنل Meshkee اینجا دیده میشود.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{products.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pageCount > 1 ? (
|
||||
<div className="mt-8 flex justify-center gap-2">
|
||||
{Array.from({ length: pageCount }, (_, index) => {
|
||||
const number = index + 1;
|
||||
return (
|
||||
<Link
|
||||
key={number}
|
||||
href={pageHref(number)}
|
||||
className={`grid size-10 place-items-center rounded-xl text-[13px] ${number === page ? "bg-brand text-white" : "border border-line bg-white text-ink"}`}
|
||||
>
|
||||
{faDigits(number)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user