Files
mashinify/src/app/user-products/[slug]/page.tsx
T
Alireza HassaniandCursor d249d958eb Add Meshkee sitemap config, technical-info labels, and footer credit.
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>
2026-08-27 07:11:52 +03:30

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>
);
}