Enhance product detail page and header with API-driven navigation.

Add gallery lightbox, tabs, marketplace CTAs, and similar products on product pages; load the business logo and level-2 category dropdown in the header.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-19 20:38:51 +03:30
co-authored by Cursor
parent 4e517fe3b2
commit 184d2f1ea4
11 changed files with 856 additions and 102 deletions
+49 -62
View File
@@ -1,31 +1,26 @@
import type { Metadata } from "next";
import Image from "next/image";
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,
listUserProducts,
productCategory,
productTitle,
} from "@/lib/api/user-products";
import { site } from "@/data/home";
import { resolveProductGallery } from "@/lib/user-product-gallery";
type PageProps = {
params: Promise<{ slug: string }>;
};
function resolveGallery(product: {
images?: { url: string }[] | null;
imageUrl?: string | null;
}) {
const fromImages =
product.images?.map((img) => img.url).filter(Boolean) ?? [];
if (fromImages.length > 0) return fromImages;
return product.imageUrl ? [product.imageUrl] : [];
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
@@ -48,13 +43,23 @@ export default async function UserProductDetailPage({ params }: PageProps) {
const category = productCategory(product);
const condition = formatCondition(product.condition);
const price = formatPrice(product.price, product.priceCurrency);
const gallery = resolveGallery(product);
const hero = gallery[0] ?? null;
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 similarResult = product.categoryId
? await listUserProducts({
categoryId: product.categoryId,
pageSize: 5,
})
: { items: [], total: 0 };
const similarProducts = similarResult.items
.filter((item) => item.id !== product.id)
.slice(0, 4);
return (
<SiteShell
banner={{
@@ -74,44 +79,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
</div>
<div className="grid gap-8 lg:grid-cols-[1.1fr_0.9fr]">
<div>
<div className="relative aspect-[4/3] overflow-hidden border border-line bg-[#e8eef4]">
{hero ? (
<Image
src={hero}
alt={title}
fill
unoptimized
className="object-cover"
sizes="(max-width: 1024px) 100vw, 55vw"
priority
/>
) : (
<div className="absolute inset-0 grid place-items-center bg-[linear-gradient(135deg,#d7e0ea,#eef3f8)] text-sm text-muted">
بدون تصویر
</div>
)}
</div>
{gallery.length > 1 ? (
<div className="mt-3 grid grid-cols-4 gap-2">
{gallery.slice(0, 8).map((url) => (
<div
key={url}
className="relative aspect-square overflow-hidden border border-line bg-[#e8eef4]"
>
<Image
src={url}
alt=""
fill
unoptimized
className="object-cover"
sizes="120px"
/>
</div>
))}
</div>
) : null}
</div>
<ProductGallery images={gallery} alt={title} />
<div>
{product.titleEn ? (
@@ -163,21 +131,40 @@ export default async function UserProductDetailPage({ params }: PageProps) {
</p>
)}
{description ? (
<p className="mt-6 whitespace-pre-line text-base leading-8 text-muted">
{description}
</p>
) : null}
<div className="mt-8 flex flex-wrap gap-3">
<div className="mt-8 flex justify-end">
<a href={`tel:${site.phone}`} className="btn-primary">
تماس برای استعلام
</a>
<Link href="/user-products" className="btn-secondary">
بازگشت به لیست
</Link> </div>
</div>
</div>
</div>
<ProductTabs
description={description}
technicalNotes={product.technicalNotes}
deliveryNotes={product.deliveryNotes}
technicalValues={product.technicalValues}
/>
{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>