{related.map((p) => (
))}
@@ -160,6 +146,9 @@ export default async function ProductDetailPage({
)}
+
+
+
>
);
}
diff --git a/src/app/products/category/[id]/[slug]/page.tsx b/src/app/products/category/[id]/[slug]/page.tsx
index dd1544e..7517e03 100644
--- a/src/app/products/category/[id]/[slug]/page.tsx
+++ b/src/app/products/category/[id]/[slug]/page.tsx
@@ -10,6 +10,7 @@ import {
getCategoryById,
getCategoryDescendantIds,
getProducts,
+ type Category,
} from "@/lib/meshkee";
import { pageMetadata } from "@/lib/seo";
import { categoryPath, categorySlug } from "@/lib/urls";
@@ -17,7 +18,7 @@ import { categoryPath, categorySlug } from "@/lib/urls";
export const revalidate = 300;
export const dynamicParams = true;
-const PAGE_SIZE = 6;
+const PAGE_SIZE = 8;
export async function generateMetadata({
params,
@@ -34,6 +35,12 @@ export async function generateMetadata({
});
}
+function chipClass(active: boolean) {
+ return active
+ ? "rounded-full bg-primary px-3.5 py-1.5 text-sm font-semibold text-white"
+ : "rounded-full bg-white px-3.5 py-1.5 text-sm font-medium text-primary hover:bg-primary/10 transition-colors";
+}
+
export default async function CategoryProductsPage({
params,
searchParams,
@@ -65,6 +72,17 @@ export default async function CategoryProductsPage({
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const currentPage = Math.min(requestedPage, totalPages);
+ const parent: Category | null = category.parentId
+ ? allCategories.find((c) => c.id === category.parentId) ?? null
+ : null;
+ const filterRoot = parent ?? category;
+ const subcategories = allCategories
+ .filter((c) => c.parentId === filterRoot.id)
+ .sort(
+ (a, b) =>
+ a.sortOrder - b.sortOrder || a.nameFa.localeCompare(b.nameFa, "fa")
+ );
+
return (
<>
-
+
-
-
- نمایش محصولات دسته{" "}
- {category.nameFa}
+
+
+ محصولات دسته{" "}
+ {filterRoot.nameFa}
+
+ {subcategories.length > 0 && (
+
+
+ همه
+
+ {subcategories.map((sub) => (
+
+ {sub.nameFa}
+
+ ))}
+
+ )}
+
همه محصولات
@@ -90,7 +131,7 @@ export default async function CategoryProductsPage({
{items.length > 0 ? (
<>
-
+
{items.map((product) => (
))}
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx
index f221596..89d3399 100644
--- a/src/app/products/page.tsx
+++ b/src/app/products/page.tsx
@@ -3,6 +3,8 @@ import Container from "@/components/Container";
import PageBanner from "@/components/PageBanner";
import ProductCard from "@/components/ProductCard";
import Pagination from "@/components/Pagination";
+import CategoryShowcase from "@/components/CategoryShowcase";
+import HomeContactStrip from "@/components/HomeContactStrip";
import { getCategoryById, getProducts } from "@/lib/meshkee";
import { pageMetadata } from "@/lib/seo";
import { categoryPath } from "@/lib/urls";
@@ -47,11 +49,11 @@ export default async function ProductsPage({
breadcrumb="محصولات"
/>
-
+
{items.length > 0 ? (
<>
-
+
{items.map((product) => (
))}
@@ -70,6 +72,9 @@ export default async function ProductsPage({
)}
+
+
+
>
);
}
diff --git a/src/components/CategoryShowcase.tsx b/src/components/CategoryShowcase.tsx
index c13e004..5091e8b 100644
--- a/src/components/CategoryShowcase.tsx
+++ b/src/components/CategoryShowcase.tsx
@@ -52,7 +52,7 @@ export default async function CategoryShowcase() {
دستهبندی محصولات
- محصول مورد نظر خود را پیدا کنید
+ دستهبندی محصولات آراد اَریسمان
diff --git a/src/components/PageBanner.tsx b/src/components/PageBanner.tsx
index fdbd569..aeaf16e 100644
--- a/src/components/PageBanner.tsx
+++ b/src/components/PageBanner.tsx
@@ -11,7 +11,7 @@ export default function PageBanner({
breadcrumb: string;
}) {
return (
-
+
- {title}
+ {title}
{subtitle && (
- {subtitle}
+ {subtitle}
)}
-
+
خانه
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx
index c3e01d4..b00f81c 100644
--- a/src/components/ProductCard.tsx
+++ b/src/components/ProductCard.tsx
@@ -14,22 +14,24 @@ export default function ProductCard({ product }: { product: Product }) {
href={productPath(product)}
className="group block rounded-2xl border border-black/12 bg-white overflow-hidden transition-colors hover:border-black"
>
-
+
{image ? (
-
+
+
+
) : (
آ
)}
{product.categoryNameFa && (
-
+
{product.categoryNameFa}
)}
diff --git a/src/components/ProductGallery.tsx b/src/components/ProductGallery.tsx
new file mode 100644
index 0000000..805c17d
--- /dev/null
+++ b/src/components/ProductGallery.tsx
@@ -0,0 +1,215 @@
+"use client";
+
+import Image from "next/image";
+import { useCallback, useEffect, useState } from "react";
+
+export default function ProductGallery({
+ images,
+ alt,
+ categoryNameFa,
+}: {
+ images: string[];
+ alt: string;
+ categoryNameFa?: string | null;
+}) {
+ const [active, setActive] = useState(0);
+ const [open, setOpen] = useState(false);
+
+ const count = images.length;
+ const current = images[active] ?? null;
+
+ const goTo = useCallback(
+ (i: number) => {
+ if (count === 0) return;
+ setActive((i + count) % count);
+ },
+ [count]
+ );
+
+ const close = useCallback(() => setOpen(false), []);
+
+ useEffect(() => {
+ if (!open) return;
+
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") close();
+ if (e.key === "ArrowLeft") goTo(active + 1);
+ if (e.key === "ArrowRight") goTo(active - 1);
+ }
+
+ const prevOverflow = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ window.addEventListener("keydown", onKey);
+ return () => {
+ document.body.style.overflow = prevOverflow;
+ window.removeEventListener("keydown", onKey);
+ };
+ }, [open, active, close, goTo]);
+
+ if (!current) {
+ return (
+
+
+ آ
+
+ {categoryNameFa && (
+
+ {categoryNameFa}
+
+ )}
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+ {count > 1 && (
+
+ {images.map((src, i) => (
+
+ ))}
+
+ )}
+
+
+ {open && (
+
+
+
+ {count > 1 && (
+ <>
+
+
+ >
+ )}
+
+
e.stopPropagation()}
+ >
+
+
+
+ {count > 1 && (
+
+ {images.map((_, i) => (
+
+ )}
+
+ )}
+ >
+ );
+}
diff --git a/src/lib/meshkee.ts b/src/lib/meshkee.ts
index 1fcca2e..8fc0b57 100644
--- a/src/lib/meshkee.ts
+++ b/src/lib/meshkee.ts
@@ -200,6 +200,18 @@ export function productImage(product: Product): string | null {
return product.thumbnail || product.image || product.thumbnailUrl || null;
}
+/** Unique gallery URLs: `images[]` first, then the primary thumbnail/image. */
+export function productImages(product: Product): string[] {
+ const seen = new Set();
+ const out: string[] = [];
+ for (const url of [...(product.images ?? []), product.thumbnail, product.image, product.thumbnailUrl]) {
+ if (!url || seen.has(url)) continue;
+ seen.add(url);
+ out.push(url);
+ }
+ return out;
+}
+
export function productPriceLabel(product: Product): string {
if (product.store?.minPrice) {
const formatted = new Intl.NumberFormat("fa-IR").format(product.store.minPrice);