Add Meshkee product catalog navigation and details

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 19:42:35 +03:30
co-authored by Cursor
parent 00126e5777
commit 34ce4f6925
13 changed files with 1499 additions and 97 deletions
@@ -0,0 +1,73 @@
"use client";
import { Maximize2, X } from "lucide-react";
import { useState } from "react";
export default function ProductGallery({
images,
title,
}: {
images: string[];
title: string;
}) {
const [activeImage, setActiveImage] = useState(images[0]);
const [lightboxOpen, setLightboxOpen] = useState(false);
return (
<>
<div className="product-gallery">
<button
className="product-gallery__main"
type="button"
onClick={() => setLightboxOpen(true)}
aria-label={`نمایش بزرگ تصویر ${title}`}
>
<img src={activeImage} alt={title} />
<span>
<Maximize2 size={18} />
مشاهده تصویر
</span>
</button>
{images.length > 1 && (
<div className="product-gallery__thumbs">
{images.map((image, index) => (
<button
className={image === activeImage ? "active" : ""}
type="button"
key={`${image}-${index}`}
onClick={() => setActiveImage(image)}
aria-label={`تصویر ${index + 1} از ${title}`}
>
<img src={image} alt={`${title}، تصویر ${index + 1}`} />
</button>
))}
</div>
)}
</div>
{lightboxOpen && (
<div
className="product-lightbox"
role="dialog"
aria-modal="true"
aria-label={`گالری ${title}`}
onClick={() => setLightboxOpen(false)}
>
<button
type="button"
onClick={() => setLightboxOpen(false)}
aria-label="بستن گالری"
>
<X />
</button>
<img
src={activeImage}
alt={title}
onClick={(event) => event.stopPropagation()}
/>
</div>
)}
</>
);
}
+211
View File
@@ -0,0 +1,211 @@
import type { Metadata } from "next";
import { ArrowLeft, CheckCircle2, Phone } from "lucide-react";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { cache } from "react";
import ProductCard from "../../../components/ProductCard";
import {
InteriorFooter,
InteriorHeader,
} from "../../../components/InteriorShell";
import {
categoryPath,
fetchProductById,
productPath,
} from "../../../lib/meshkee";
import ProductGallery from "./ProductGallery";
export const dynamic = "force-dynamic";
type ProductPageProps = {
params: Promise<{ id: string; slug: string }>;
};
const getProduct = cache(async (id: string) => fetchProductById(id));
function plainText(value?: string | null) {
return (value || "")
.replace(/<[^>]*>/g, " ")
.replace(/&nbsp;/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export async function generateMetadata({
params,
}: ProductPageProps): Promise<Metadata> {
const { id } = await params;
const data = await getProduct(id).catch(() => undefined);
if (!data?.product) {
return {
title: "محصول یافت نشد | سامان اندیش",
};
}
const product = data.product;
const title = product.nameFa || product.title;
const description =
product.summary ||
plainText(product.descriptionHtml) ||
`مشخصات و تصاویر ${title} را مشاهده کنید و برای دریافت اطلاعات فنی و استعلام قیمت با کارشناسان سامان اندیش تماس بگیرید.`;
const image = product.thumbnailUrl || product.image || product.thumbnail;
return {
title: `${title} | سامان اندیش`,
description: description.slice(0, 160),
alternates: {
canonical: productPath(product),
},
openGraph: {
title: `${title} | سامان اندیش`,
description: description.slice(0, 160),
url: productPath(product),
type: "website",
images: image ? [{ url: image, alt: title }] : undefined,
},
};
}
export default async function ProductDetailPage({
params,
}: ProductPageProps) {
const { id, slug } = await params;
const data = await getProduct(id).catch(() => undefined);
if (!data?.product) notFound();
const product = data.product;
const canonicalPath = productPath(product);
const requestedSlug = decodeURIComponent(slug);
if (!canonicalPath.endsWith(`/${requestedSlug}`)) {
redirect(encodeURI(canonicalPath));
}
const title = product.nameFa || product.title;
const categoryTitle =
product.categoryNameFa || product.categoryName || "ماشین‌آلات صنعتی";
const categoryHref =
product.categoryId && (product.categoryNameFa || product.categoryName)
? categoryPath({
id: product.categoryId,
nameFa: product.categoryNameFa,
name: product.categoryName || categoryTitle,
})
: "/products";
const images = Array.from(
new Set(
[
product.thumbnailUrl,
product.image,
product.thumbnail,
...(product.images || []).map((image) => image.url),
].filter((image): image is string => Boolean(image)),
),
);
if (!images.length) images.push("/assets/product-quadding.jpg");
const description =
plainText(product.descriptionHtml) ||
product.summary ||
"برای دریافت توضیحات فنی کامل این دستگاه با کارشناسان سامان اندیش تماس بگیرید.";
const relatedProducts = (data.relatedProducts || []).slice(0, 4);
return (
<main>
<InteriorHeader active="products" />
<div className="product-detail-breadcrumb">
<div className="container">
<Link href="/">صفحه اصلی</Link>
<i>/</i>
<Link href="/products">ماشین‌آلات</Link>
<i>/</i>
<Link href={categoryHref}>{categoryTitle}</Link>
<i>/</i>
<strong>{title}</strong>
</div>
</div>
<section className="product-detail">
<div className="container product-detail__grid">
<ProductGallery images={images} title={title} />
<div className="product-detail__info">
<span className="inner-eyebrow">INDUSTRIAL MACHINERY</span>
<Link className="product-detail__category" href={categoryHref}>
{categoryTitle}
</Link>
<h1>{title}</h1>
<p className="product-detail__english" dir="ltr">
{product.title}
</p>
<p className="product-detail__summary">{description}</p>
<div className="product-detail__features">
<span>
<CheckCircle2 />
ارزیابی فنی پیش از خرید
</span>
<span>
<CheckCircle2 />
پشتیبانی نصب و راه‌اندازی
</span>
</div>
<div className="product-detail__actions">
<a className="button button--red" href="tel:+982188530256">
<Phone size={18} />
استعلام قیمت
</a>
<Link className="button button--navy" href="/contact-us">
درخواست مشاوره
<ArrowLeft size={18} />
</Link>
</div>
</div>
</div>
</section>
<section className="product-description">
<div className="container product-description__grid">
<article>
<span>PRODUCT DESCRIPTION</span>
<h2>توضیحات محصول</h2>
<p>{description}</p>
</article>
<aside>
<h2>نیاز به اطلاعات بیشتر دارید؟</h2>
<p>
مشخصات فنی، وضعیت دستگاه و شرایط تأمین را مستقیماً از کارشناسان
سامان اندیش دریافت کنید.
</p>
<a href="tel:+982188530256" dir="ltr">
021 8853 0256
</a>
</aside>
</div>
</section>
{relatedProducts.length > 0 && (
<section className="related-products section-gray">
<div className="container">
<div className="product-listing__heading">
<div>
<span>RELATED PRODUCTS</span>
<h2>ماشین‌آلات مرتبط</h2>
</div>
</div>
<div className="product-grid">
{relatedProducts.map((relatedProduct) => (
<ProductCard key={relatedProduct.id} product={relatedProduct} />
))}
</div>
</div>
</section>
)}
<InteriorFooter />
</main>
);
}
+114
View File
@@ -0,0 +1,114 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import ProductCard from "../../../../components/ProductCard";
import {
InteriorFooter,
InteriorHeader,
} from "../../../../components/InteriorShell";
import {
categoryPath,
fetchCategoryById,
fetchLatestProducts,
} from "../../../../lib/meshkee";
export const dynamic = "force-dynamic";
type CategoryPageProps = {
params: Promise<{ id: string; slug: string }>;
};
export async function generateMetadata({
params,
}: CategoryPageProps): Promise<Metadata> {
const { id } = await params;
const category = await fetchCategoryById(id).catch(() => undefined);
if (!category) {
return {
title: "دسته‌بندی یافت نشد | سامان اندیش",
};
}
const title = category.nameFa || category.name;
return {
title: `${title} | ماشین‌آلات سامان اندیش`,
description:
category.description ||
`جدیدترین محصولات و ماشین‌آلات دسته ${title} را مشاهده کنید و برای دریافت مشخصات فنی، مشاوره تخصصی و استعلام قیمت با سامان اندیش تماس بگیرید.`,
alternates: {
canonical: categoryPath(category),
},
};
}
export default async function ProductCategoryPage({
params,
}: CategoryPageProps) {
const { id, slug } = await params;
const [category, products] = await Promise.all([
fetchCategoryById(id).catch(() => undefined),
fetchLatestProducts(48, undefined, id).catch(() => []),
]);
if (!category) notFound();
const canonicalPath = categoryPath(category);
const requestedSlug = decodeURIComponent(slug);
if (!canonicalPath.endsWith(`/${requestedSlug}`)) {
redirect(encodeURI(canonicalPath));
}
const title = category.nameFa || category.name;
return (
<main>
<InteriorHeader active="products" />
<section
className="inner-hero product-list-hero"
style={{ backgroundImage: 'url("/assets/s9zJRbKrhE.jpg")' }}
>
<div className="inner-hero__shade" />
<div className="container inner-hero__content">
<span dir="ltr">{category.name}</span>
<h1>{title}</h1>
<p>ماشین‌آلات و تجهیزات تخصصی این دسته‌بندی</p>
<div>
<Link href="/">صفحه اصلی</Link>
<i>/</i>
<Link href="/products">ماشین‌آلات</Link>
<i>/</i>
<strong>{title}</strong>
</div>
</div>
</section>
<section className="product-listing">
<div className="container">
<div className="product-listing__heading">
<div>
<span>CATEGORY PRODUCTS</span>
<h2>محصولات {title}</h2>
</div>
<p>{products.length} محصول</p>
</div>
{products.length ? (
<div className="product-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="products-empty">
محصولی در این دسته‌بندی ثبت نشده است.
</div>
)}
</div>
</section>
<InteriorFooter />
</main>
);
}
+69
View File
@@ -0,0 +1,69 @@
import type { Metadata } from "next";
import Link from "next/link";
import ProductCard from "../components/ProductCard";
import { InteriorFooter, InteriorHeader } from "../components/InteriorShell";
import { fetchLatestProducts } from "../lib/meshkee";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "ماشین‌آلات صنعت سیم و کابل | سامان اندیش",
description:
"جدیدترین ماشین‌آلات و تجهیزات خطوط تولید سیم و کابل سامان اندیش را مشاهده کنید و برای انتخاب، ارزیابی فنی و استعلام قیمت با کارشناسان ما در ارتباط باشید.",
alternates: {
canonical: "/products",
},
};
export default async function ProductsPage() {
const products = await fetchLatestProducts(24).catch(() => []);
return (
<main>
<InteriorHeader active="products" />
<section
className="inner-hero product-list-hero"
style={{ backgroundImage: 'url("/assets/i8SsUDDMLv.jpg")' }}
>
<div className="inner-hero__shade" />
<div className="container inner-hero__content">
<span>INDUSTRIAL MACHINERY</span>
<h1>ماشین‌آلات سیم و کابل</h1>
<p>جدیدترین تجهیزات موجود برای خطوط تولید صنعتی</p>
<div>
<Link href="/">صفحه اصلی</Link>
<i>/</i>
<strong>ماشین‌آلات</strong>
</div>
</div>
</section>
<section className="product-listing">
<div className="container">
<div className="product-listing__heading">
<div>
<span>NEWEST PRODUCTS</span>
<h2>جدیدترین ماشین‌آلات</h2>
</div>
<p>{products.length} محصول اخیر</p>
</div>
{products.length ? (
<div className="product-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="products-empty">
در حال حاضر محصولی برای نمایش در دسترس نیست.
</div>
)}
</div>
</section>
<InteriorFooter />
</main>
);
}