Add Meshkee product catalog navigation and details
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
00126e5777
commit
34ce4f6925
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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(/ /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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user