Wire specs to /technical-info for field labels, match Safeteb footer credit, and publish build-time /meshkee/sitemap-config.json for SEO sync. Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
6.4 KiB
TypeScript
186 lines
6.4 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import { ProductGallery } from "@/components/ProductGallery";
|
|
import { ProductMarketplaceCtas } from "@/components/ProductMarketplaceCtas";
|
|
import { ProductTabs } from "@/components/ProductTabs";
|
|
import { Products } from "@/components/Products";
|
|
import { SiteShell } from "@/components/SiteShell";
|
|
import { site } from "@/data/home";
|
|
import {
|
|
formatCondition,
|
|
formatPrice,
|
|
getUserProductBySlugOrNull,
|
|
getUserProductTechnicalInfo,
|
|
listUserProducts,
|
|
productCategory,
|
|
productTitle,
|
|
} from "@/lib/api/user-products";
|
|
import { resolveProductGallery } from "@/lib/user-product-gallery";
|
|
|
|
type PageProps = {
|
|
params: Promise<{ slug: string }>;
|
|
};
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: PageProps): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const product = await getUserProductBySlugOrNull(slug);
|
|
if (!product) return { title: "محصول یافت نشد" };
|
|
const title = productTitle(product);
|
|
const description =
|
|
product.descriptionFa || product.description || product.descriptionEn || title;
|
|
const image = product.imageUrl || product.images?.[0]?.url || undefined;
|
|
return {
|
|
title,
|
|
description,
|
|
openGraph: {
|
|
title,
|
|
description,
|
|
...(image ? { images: [{ url: image, alt: title }] } : {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function UserProductDetailPage({ params }: PageProps) {
|
|
const { slug } = await params;
|
|
const product = await getUserProductBySlugOrNull(slug);
|
|
if (!product) notFound();
|
|
|
|
const title = productTitle(product);
|
|
const category = productCategory(product);
|
|
const condition = formatCondition(product.condition);
|
|
const price = formatPrice(product.price, product.priceCurrency);
|
|
const gallery = resolveProductGallery(product);
|
|
const city = product.cityNameFa || product.cityName || null;
|
|
const country = product.countryNameFa || product.countryName || null;
|
|
const description =
|
|
product.descriptionFa || product.description || product.descriptionEn;
|
|
const deliveryNotes = product.deliveryNotes ?? product.deliveryNote ?? null;
|
|
const [technicalInfo, similarResult] = await Promise.all([
|
|
getUserProductTechnicalInfo(slug),
|
|
product.categoryId
|
|
? listUserProducts({
|
|
categoryId: product.categoryId,
|
|
pageSize: 5,
|
|
})
|
|
: Promise.resolve({ items: [], total: 0 }),
|
|
]);
|
|
|
|
const similarProducts = similarResult.items
|
|
.filter((item) => item.id !== product.id)
|
|
.slice(0, 4);
|
|
|
|
return (
|
|
<SiteShell
|
|
banner={{
|
|
kicker: "Product Details",
|
|
title,
|
|
description: [category, condition].filter(Boolean).join(" · ") || undefined,
|
|
}}
|
|
>
|
|
<article className="section">
|
|
<div className="container-page">
|
|
<div className="mb-6 text-sm text-muted">
|
|
<Link href="/user-products" className="text-brand hover:underline">
|
|
محصولات
|
|
</Link>
|
|
<span className="mx-2">/</span>
|
|
<span>{title}</span>
|
|
</div>
|
|
|
|
<div className="grid gap-8 lg:grid-cols-[1.1fr_0.9fr]">
|
|
<ProductGallery images={gallery} alt={title} />
|
|
|
|
<div>
|
|
{product.titleEn ? (
|
|
<p className="font-industrial text-sm tracking-[0.14em] text-brand">
|
|
{product.titleEn}
|
|
</p>
|
|
) : null}
|
|
<h2 className="mt-2 text-3xl font-extrabold leading-tight text-brand-ink md:text-4xl">
|
|
{title}
|
|
</h2>
|
|
{category ? (
|
|
<p className="mt-3 text-sm font-bold text-muted">{category}</p>
|
|
) : null}
|
|
|
|
<div className="mt-6 grid gap-3 sm:grid-cols-2">
|
|
{condition ? (
|
|
<div className="border border-brand/25 bg-brand/[0.06] px-4 py-3">
|
|
<p className="font-industrial text-[0.65rem] tracking-[0.16em] text-brand">
|
|
CONDITION
|
|
</p>
|
|
<p className="mt-1 text-base font-extrabold text-brand-ink">
|
|
{condition}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
|
|
{city || country ? (
|
|
<div className="border border-line bg-[#f7fafc] px-4 py-3">
|
|
<p className="font-industrial text-[0.65rem] tracking-[0.16em] text-steel">
|
|
LOCATION
|
|
</p>
|
|
<p className="mt-1 text-base font-extrabold text-brand-ink">
|
|
{[city, country].filter(Boolean).join("، ")}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
{price ? (
|
|
<p
|
|
dir="ltr"
|
|
className="font-industrial mt-6 text-left text-2xl font-extrabold tracking-wide text-brand"
|
|
>
|
|
{price}
|
|
</p>
|
|
) : (
|
|
<p className="mt-6 text-base font-bold text-muted">
|
|
قیمت با استعلام
|
|
</p>
|
|
)}
|
|
|
|
<div className="mt-8 flex justify-end">
|
|
<a href={`tel:${site.phone}`} className="btn-primary">
|
|
تماس برای استعلام
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ProductTabs
|
|
description={description}
|
|
technicalNotes={product.technicalNotes}
|
|
deliveryNotes={deliveryNotes}
|
|
technicalValues={product.technicalValues}
|
|
technicalInfo={technicalInfo}
|
|
/>
|
|
|
|
{similarProducts.length > 0 ? (
|
|
<Products
|
|
embedded
|
|
items={similarProducts}
|
|
total={similarResult.total}
|
|
kicker="Similar Products"
|
|
title="محصولات مشابه"
|
|
description="سایر ماشینآلات موجود در همین دستهبندی که ممکن است برای شما مناسب باشند."
|
|
showMoreLink={false}
|
|
titleHref={
|
|
product.categoryId
|
|
? `/user-products?categoryId=${product.categoryId}`
|
|
: "/user-products"
|
|
}
|
|
moreLabel="مشاهده همه در این دسته"
|
|
/>
|
|
) : null}
|
|
|
|
<ProductMarketplaceCtas />
|
|
</div>
|
|
</article>
|
|
</SiteShell>
|
|
);
|
|
}
|