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:
co-authored by
Cursor
parent
4e517fe3b2
commit
184d2f1ea4
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user