Polish storefront listing: filters, category directory, and list extras.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-27 20:46:17 +03:30
co-authored by Cursor
parent 4089247ad3
commit fa248d2f6a
26 changed files with 1274 additions and 274 deletions
+117 -19
View File
@@ -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
View File
@@ -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>