Polish storefront listing: filters, category directory, and list extras.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
4089247ad3
commit
fa248d2f6a
@@ -8,19 +8,32 @@ 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 ProductEmptyState from "@/components/products/ProductEmptyState";
|
||||
import ProductListExtras from "@/components/products/ProductListExtras";
|
||||
import { getCategoryPage } from "@/data/pages";
|
||||
import { mapHomeSlots } from "@/lib/map-home-slots";
|
||||
import { fetchPublicWebsiteSlots } from "@/lib/website-slots";
|
||||
import {
|
||||
fetchPublicWebsiteSlots,
|
||||
itemsForSlot,
|
||||
} from "@/lib/website-slots";
|
||||
import { getStorefrontProducts } from "@/lib/products-api";
|
||||
import {
|
||||
categoryAncestry,
|
||||
fetchPublicCategories,
|
||||
mapCategoriesToMegaTabs,
|
||||
resolveHomeCategoryStrip,
|
||||
type ApiCategoryNode,
|
||||
} from "@/lib/categories-api";
|
||||
import { categoryPath, categorySlug } from "@/lib/category-path";
|
||||
import {
|
||||
paginateItems,
|
||||
parseProductListQuery,
|
||||
type ProductListQuery,
|
||||
} from "@/lib/products-query";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ path: string[] }>;
|
||||
searchParams: Promise<ProductListQuery>;
|
||||
};
|
||||
|
||||
function findCategory(
|
||||
@@ -47,7 +60,8 @@ async function resolveCategory(id: string) {
|
||||
nameFa: apiCat.nameFa,
|
||||
nameEn: apiCat.nameEn || apiCat.nameFa,
|
||||
imageUrl: apiCat.imageUrl,
|
||||
description: `مجموعهای از محصولات ${apiCat.nameFa} در مانا کالکشن.`,
|
||||
seoDescription: (apiCat.seoDescription ?? "").trim(),
|
||||
description: (apiCat.seoDescription ?? "").trim(),
|
||||
};
|
||||
}
|
||||
const staticCat = getCategoryPage(id);
|
||||
@@ -60,6 +74,7 @@ async function resolveCategory(id: string) {
|
||||
nameFa: staticCat.nameFa,
|
||||
nameEn: staticCat.nameEn,
|
||||
imageUrl: null as string | null,
|
||||
seoDescription: "",
|
||||
description: staticCat.description,
|
||||
};
|
||||
}
|
||||
@@ -76,11 +91,13 @@ export async function generateMetadata({
|
||||
if (!resolved) return { title: "دسته | مانا کالکشن" };
|
||||
return {
|
||||
title: `${resolved.nameFa} | مانا کالکشن`,
|
||||
description: resolved.description,
|
||||
...(resolved.description
|
||||
? { description: resolved.description }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function CategoryPage({ params }: PageProps) {
|
||||
export default async function CategoryPage({ params, searchParams }: PageProps) {
|
||||
const { path } = await params;
|
||||
if (!path?.length || path.length > 2) notFound();
|
||||
|
||||
@@ -104,10 +121,22 @@ export default async function CategoryPage({ params }: PageProps) {
|
||||
redirect(canonical);
|
||||
}
|
||||
|
||||
const query = parseProductListQuery(await searchParams);
|
||||
const [slotItems, products] = await Promise.all([
|
||||
fetchPublicWebsiteSlots(),
|
||||
getStorefrontProducts(
|
||||
resolved.apiCat ? { categoryId: resolved.id } : undefined,
|
||||
resolved.apiCat
|
||||
? {
|
||||
categoryId: resolved.id,
|
||||
name: query.q || undefined,
|
||||
minPrice: query.minPrice,
|
||||
maxPrice: query.maxPrice,
|
||||
}
|
||||
: {
|
||||
name: query.q || undefined,
|
||||
minPrice: query.minPrice,
|
||||
maxPrice: query.maxPrice,
|
||||
},
|
||||
),
|
||||
]);
|
||||
const home = mapHomeSlots(slotItems);
|
||||
@@ -116,6 +145,35 @@ export default async function CategoryPage({ params }: PageProps) {
|
||||
home.megaTabs.map((t) => ({ image: t.image })),
|
||||
);
|
||||
|
||||
// Same category strip source as home
|
||||
const stripCategories = resolveHomeCategoryStrip(
|
||||
resolved.apiCategories,
|
||||
home.categories,
|
||||
itemsForSlot(slotItems, "category-strip").length > 0,
|
||||
);
|
||||
|
||||
const ancestry = categoryAncestry(resolved.apiCategories, resolved.id);
|
||||
const breadcrumbs = [
|
||||
{ label: "خانه", href: "/" },
|
||||
...(ancestry.length
|
||||
? ancestry.map((node, index) => ({
|
||||
label: node.nameFa,
|
||||
...(index < ancestry.length - 1
|
||||
? { href: categoryPath(node) }
|
||||
: {}),
|
||||
}))
|
||||
: [
|
||||
{ label: "دستهبندیها", href: "/categories" },
|
||||
{ label: resolved.nameFa },
|
||||
]),
|
||||
];
|
||||
|
||||
const { pageItems, totalPages, page, total } = paginateItems(
|
||||
products,
|
||||
query.page,
|
||||
);
|
||||
const hasExtraFilters = Boolean(query.q || query.min || query.max);
|
||||
|
||||
return (
|
||||
<main className="home-page min-h-dvh bg-white text-ink">
|
||||
<div className="relative">
|
||||
@@ -128,25 +186,50 @@ export default async function CategoryPage({ params }: PageProps) {
|
||||
resolved.imageUrl ||
|
||||
resolved.staticCat?.heroImage
|
||||
}
|
||||
breadcrumbs={[
|
||||
{ label: "خانه", href: "/" },
|
||||
{ label: "دستهبندیها", href: "/categories" },
|
||||
{ label: resolved.nameFa },
|
||||
]}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
</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 />
|
||||
<ProductFilters
|
||||
basePath={canonical}
|
||||
lockedCategory={{ id: resolved.id, nameFa: resolved.nameFa }}
|
||||
initial={{
|
||||
q: query.q,
|
||||
min: query.min,
|
||||
max: query.max,
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-3 sm:space-y-4">
|
||||
<ProductGrid products={products} />
|
||||
<ProductPagination />
|
||||
{total === 0 ? (
|
||||
<ProductEmptyState
|
||||
title={
|
||||
hasExtraFilters
|
||||
? "محصولی با این فیلترها پیدا نشد"
|
||||
: `محصولی در «${resolved.nameFa}» نیست`
|
||||
}
|
||||
description={
|
||||
hasExtraFilters
|
||||
? "فیلترها را تغییر دهید یا همه را پاک کنید تا محصولات بیشتری ببینید."
|
||||
: "بهزودی محصولات این دسته اضافه میشوند."
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ProductGrid products={pageItems} />
|
||||
<ProductPagination
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
basePath={canonical}
|
||||
query={{
|
||||
q: query.q,
|
||||
min: query.min,
|
||||
max: query.max,
|
||||
}}
|
||||
lockedCategoryId={resolved.id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +241,21 @@ export default async function CategoryPage({ params }: PageProps) {
|
||||
link={home.leather.link}
|
||||
/>
|
||||
<PartnerCta />
|
||||
|
||||
<ProductListExtras
|
||||
stripCategories={stripCategories}
|
||||
about={home.about}
|
||||
categorySeo={
|
||||
resolved.seoDescription
|
||||
? {
|
||||
nameFa: resolved.nameFa,
|
||||
nameEn: resolved.nameEn,
|
||||
description: resolved.seoDescription,
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
|
||||
<SiteFooter paymentBadges={home.paymentBadges} />
|
||||
</main>
|
||||
);
|
||||
|
||||
+19
-40
@@ -1,19 +1,18 @@
|
||||
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 LeatherBanner from "@/components/home/LeatherBanner";
|
||||
import PartnerCta from "@/components/home/PartnerCta";
|
||||
import ProductListHero from "@/components/products/ProductListHero";
|
||||
import CategoriesIndexDirectory from "@/components/categories/CategoriesIndexDirectory";
|
||||
import BilingualTitle from "@/components/ui/BilingualTitle";
|
||||
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: "دستهبندیها | مانا کالکشن",
|
||||
@@ -26,7 +25,6 @@ export default async function CategoriesIndexPage() {
|
||||
fetchPublicCategories(),
|
||||
]);
|
||||
const home = mapHomeSlots(slotItems);
|
||||
const list = mapCategoriesToStrip(apiCategories);
|
||||
const megaTabs = mapCategoriesToMegaTabs(
|
||||
apiCategories,
|
||||
home.megaTabs.map((t) => ({ image: t.image })),
|
||||
@@ -47,43 +45,24 @@ export default async function CategoriesIndexPage() {
|
||||
/>
|
||||
</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 className="mx-auto w-full max-w-[1920px] px-4 pt-8 sm:px-6 sm:pt-10 lg:px-[120px]">
|
||||
<BilingualTitle
|
||||
titleFa="دستهبندیها"
|
||||
titleEn="Categories"
|
||||
size="lg"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CategoriesIndexDirectory categories={apiCategories} />
|
||||
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user