Connect storefront to backend catalog, search, and home slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
2f857f1a90
commit
0074ab6629
@@ -0,0 +1,164 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import SiteHeader from "@/components/home/SiteHeader";
|
||||
import SiteFooter from "@/components/home/SiteFooter";
|
||||
import LeatherBanner from "@/components/home/LeatherBanner";
|
||||
import PartnerCta from "@/components/home/PartnerCta";
|
||||
import ProductListHero from "@/components/products/ProductListHero";
|
||||
import ProductFilters from "@/components/products/ProductFilters";
|
||||
import ProductGrid from "@/components/products/ProductGrid";
|
||||
import ProductPagination from "@/components/products/ProductPagination";
|
||||
import { getCategoryPage } from "@/data/pages";
|
||||
import { mapHomeSlots } from "@/lib/map-home-slots";
|
||||
import { fetchPublicWebsiteSlots } from "@/lib/website-slots";
|
||||
import { getStorefrontProducts } from "@/lib/products-api";
|
||||
import {
|
||||
fetchPublicCategories,
|
||||
mapCategoriesToMegaTabs,
|
||||
type ApiCategoryNode,
|
||||
} from "@/lib/categories-api";
|
||||
import { categoryPath, categorySlug } from "@/lib/category-path";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ path: string[] }>;
|
||||
};
|
||||
|
||||
function findCategory(
|
||||
nodes: ApiCategoryNode[],
|
||||
id: string,
|
||||
): ApiCategoryNode | null {
|
||||
for (const node of nodes) {
|
||||
if (String(node.id) === id) return node;
|
||||
const child = findCategory(node.children ?? [], id);
|
||||
if (child) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveCategory(id: string) {
|
||||
const apiCategories = await fetchPublicCategories();
|
||||
const apiCat = findCategory(apiCategories, id);
|
||||
if (apiCat) {
|
||||
return {
|
||||
apiCategories,
|
||||
apiCat,
|
||||
staticCat: null as ReturnType<typeof getCategoryPage>,
|
||||
id: apiCat.id,
|
||||
nameFa: apiCat.nameFa,
|
||||
nameEn: apiCat.nameEn || apiCat.nameFa,
|
||||
imageUrl: apiCat.imageUrl,
|
||||
description: `مجموعهای از محصولات ${apiCat.nameFa} در مانا کالکشن.`,
|
||||
};
|
||||
}
|
||||
const staticCat = getCategoryPage(id);
|
||||
if (staticCat) {
|
||||
return {
|
||||
apiCategories,
|
||||
apiCat: null,
|
||||
staticCat,
|
||||
id: staticCat.id,
|
||||
nameFa: staticCat.nameFa,
|
||||
nameEn: staticCat.nameEn,
|
||||
imageUrl: null as string | null,
|
||||
description: staticCat.description,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
const { path } = await params;
|
||||
const id = path?.[0];
|
||||
if (!id) return { title: "دسته | مانا کالکشن" };
|
||||
const resolved = await resolveCategory(id);
|
||||
if (!resolved) return { title: "دسته | مانا کالکشن" };
|
||||
return {
|
||||
title: `${resolved.nameFa} | مانا کالکشن`,
|
||||
description: resolved.description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CategoryPage({ params }: PageProps) {
|
||||
const { path } = await params;
|
||||
if (!path?.length || path.length > 2) notFound();
|
||||
|
||||
const [id, slug] = path;
|
||||
const resolved = await resolveCategory(id);
|
||||
if (!resolved) notFound();
|
||||
|
||||
const expectedSlug = categorySlug({
|
||||
id: resolved.id,
|
||||
nameEn: resolved.nameEn,
|
||||
nameFa: resolved.nameFa,
|
||||
});
|
||||
const canonical = categoryPath({
|
||||
id: resolved.id,
|
||||
nameEn: resolved.nameEn,
|
||||
nameFa: resolved.nameFa,
|
||||
});
|
||||
|
||||
// Legacy `/categories/:id` or wrong slug → canonical `/categories/:id/:slug`
|
||||
if (path.length === 1 || slug !== expectedSlug) {
|
||||
redirect(canonical);
|
||||
}
|
||||
|
||||
const [slotItems, products] = await Promise.all([
|
||||
fetchPublicWebsiteSlots(),
|
||||
getStorefrontProducts(
|
||||
resolved.apiCat ? { categoryId: resolved.id } : undefined,
|
||||
),
|
||||
]);
|
||||
const home = mapHomeSlots(slotItems);
|
||||
const megaTabs = mapCategoriesToMegaTabs(
|
||||
resolved.apiCategories,
|
||||
home.megaTabs.map((t) => ({ image: t.image })),
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="home-page min-h-dvh bg-white text-ink">
|
||||
<div className="relative">
|
||||
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
|
||||
<ProductListHero
|
||||
titleFa={resolved.nameFa}
|
||||
titleEn={resolved.nameEn}
|
||||
heroImage={
|
||||
home.heroSlides[0]?.image ||
|
||||
resolved.imageUrl ||
|
||||
resolved.staticCat?.heroImage
|
||||
}
|
||||
breadcrumbs={[
|
||||
{ label: "خانه", href: "/" },
|
||||
{ label: "دستهبندیها", href: "/categories" },
|
||||
{ label: resolved.nameFa },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-[1200px] px-4 pt-8 sm:px-6 lg:px-8">
|
||||
<p className="max-w-2xl text-sm leading-7 text-ink/75">
|
||||
{resolved.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 space-y-6 sm:mt-8 lg:mt-[48px]">
|
||||
<ProductFilters />
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<ProductGrid products={products} />
|
||||
<ProductPagination />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LeatherBanner
|
||||
image={home.leather.image}
|
||||
nameFa={home.leather.nameFa}
|
||||
nameEn={home.leather.nameEn}
|
||||
linkText={home.leather.linkText}
|
||||
link={home.leather.link}
|
||||
/>
|
||||
<PartnerCta />
|
||||
<SiteFooter paymentBadges={home.paymentBadges} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import SiteHeader from "@/components/home/SiteHeader";
|
||||
import SiteFooter from "@/components/home/SiteFooter";
|
||||
import PartnerCta from "@/components/home/PartnerCta";
|
||||
import ProductListHero from "@/components/products/ProductListHero";
|
||||
import { LIST_PAGE_META } from "@/data/products";
|
||||
import { mapHomeSlots } from "@/lib/map-home-slots";
|
||||
import { fetchPublicWebsiteSlots } from "@/lib/website-slots";
|
||||
import {
|
||||
fetchPublicCategories,
|
||||
mapCategoriesToMegaTabs,
|
||||
mapCategoriesToStrip,
|
||||
} from "@/lib/categories-api";
|
||||
import { categoryPath } from "@/lib/category-path";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "دستهبندیها | مانا کالکشن",
|
||||
description: "مرور دستهبندیهای مانا کالکشن",
|
||||
};
|
||||
|
||||
export default async function CategoriesIndexPage() {
|
||||
const [slotItems, apiCategories] = await Promise.all([
|
||||
fetchPublicWebsiteSlots(),
|
||||
fetchPublicCategories(),
|
||||
]);
|
||||
const home = mapHomeSlots(slotItems);
|
||||
const list = mapCategoriesToStrip(apiCategories);
|
||||
const megaTabs = mapCategoriesToMegaTabs(
|
||||
apiCategories,
|
||||
home.megaTabs.map((t) => ({ image: t.image })),
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="home-page min-h-dvh bg-white text-ink">
|
||||
<div className="relative">
|
||||
<SiteHeader megaTabs={megaTabs.length ? megaTabs : home.megaTabs} />
|
||||
<ProductListHero
|
||||
titleFa="دستهبندیها"
|
||||
titleEn="Categories"
|
||||
heroImage={home.heroSlides[0]?.image || LIST_PAGE_META.heroImage}
|
||||
breadcrumbs={[
|
||||
{ label: "خانه", href: "/" },
|
||||
{ label: "دستهبندیها" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-[1200px] px-4 py-12 sm:px-6 sm:py-16 lg:px-8">
|
||||
<ul className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-3 lg:gap-6">
|
||||
{list.map((cat) => (
|
||||
<li key={cat.id}>
|
||||
<Link
|
||||
href={cat.href || categoryPath(cat)}
|
||||
className="group block overflow-hidden rounded-[5px] border border-line bg-white transition-colors hover:border-ink"
|
||||
>
|
||||
<div className="relative aspect-[4/5] overflow-hidden bg-[#f6f6f4]">
|
||||
<Image
|
||||
src={cat.image}
|
||||
alt={cat.nameFa}
|
||||
fill
|
||||
sizes="(max-width: 768px) 50vw, 360px"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-[1.03]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end justify-between gap-3 px-4 py-3">
|
||||
<div>
|
||||
<p className="text-base text-ink">{cat.nameFa}</p>
|
||||
<p
|
||||
dir="ltr"
|
||||
className="mt-1 font-montserrat text-[10px] uppercase tracking-[0.16em] text-muted"
|
||||
>
|
||||
{cat.nameEn}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted transition-colors group-hover:text-ink">
|
||||
مشاهده
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<PartnerCta />
|
||||
<SiteFooter paymentBadges={home.paymentBadges} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user