From 184d2f1ea45cd3a6c27f03599ba4a4ad975a74e1 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 19 Aug 2026 20:38:51 +0330 Subject: [PATCH] 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 --- src/app/page.tsx | 4 +- src/app/user-products/[slug]/page.tsx | 111 +++++----- src/components/Header.tsx | 243 +++++++++++++++++++--- src/components/HeaderContainer.tsx | 22 ++ src/components/ProductGallery.tsx | 177 ++++++++++++++++ src/components/ProductMarketplaceCtas.tsx | 72 +++++++ src/components/ProductTabs.tsx | 213 +++++++++++++++++++ src/components/Products.tsx | 75 ++++++- src/components/SiteShell.tsx | 4 +- src/lib/api/categories.ts | 28 +++ src/lib/user-product-gallery.ts | 9 + 11 files changed, 856 insertions(+), 102 deletions(-) create mode 100644 src/components/HeaderContainer.tsx create mode 100644 src/components/ProductGallery.tsx create mode 100644 src/components/ProductMarketplaceCtas.tsx create mode 100644 src/components/ProductTabs.tsx create mode 100644 src/lib/user-product-gallery.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index 706d063..2622492 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,7 +2,7 @@ import { AboutCta } from "@/components/AboutCta"; import { Articles } from "@/components/Articles"; import { Categories } from "@/components/Categories"; import { Footer } from "@/components/Footer"; -import { Header } from "@/components/Header"; +import { HeaderContainer } from "@/components/HeaderContainer"; import { Hero } from "@/components/Hero"; import { Products } from "@/components/Products"; import { SellCta } from "@/components/SellCta"; @@ -17,7 +17,7 @@ export default async function Home() { return ( <> -
+
diff --git a/src/app/user-products/[slug]/page.tsx b/src/app/user-products/[slug]/page.tsx index 234aa39..5df4549 100644 --- a/src/app/user-products/[slug]/page.tsx +++ b/src/app/user-products/[slug]/page.tsx @@ -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 { @@ -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 (
-
-
- {hero ? ( - {title} - ) : ( -
- بدون تصویر -
- )} -
- {gallery.length > 1 ? ( -
- {gallery.slice(0, 8).map((url) => ( -
- -
- ))} -
- ) : null} -
+
{product.titleEn ? ( @@ -163,21 +131,40 @@ export default async function UserProductDetailPage({ params }: PageProps) {

)} - {description ? ( -

- {description} -

- ) : null} - -
+
تماس برای استعلام - - بازگشت به لیست -
+
+ + + + {similarProducts.length > 0 ? ( + + ) : null} + +
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4819ec5..c439ed4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,15 +1,165 @@ "use client"; -import { useEffect, useState } from "react"; +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; import { navItems, site } from "@/data/home"; +export type NavProductCategory = { + id: string; + label: string; + href: string; +}; + type HeaderProps = { forceSolid?: boolean; + productCategories?: NavProductCategory[]; + logoUrl?: string | null; }; -export function Header({ forceSolid = false }: HeaderProps) { +function ChevronDown({ open }: { open?: boolean }) { + return ( + + + + ); +} + +function ProductsNavDropdown({ + categories, + onNavigate, + variant = "desktop", +}: { + categories: NavProductCategory[]; + onNavigate?: () => void; + variant?: "desktop" | "mobile"; +}) { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (variant !== "desktop" || !open) return; + + const onPointerDown = (event: MouseEvent) => { + if (!containerRef.current?.contains(event.target as Node)) { + setOpen(false); + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setOpen(false); + }; + + document.addEventListener("mousedown", onPointerDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("mousedown", onPointerDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [open, variant]); + + if (variant === "mobile") { + return ( +
+ + {open ? ( +
+ + همه محصولات + + {categories.map((category) => ( + + {category.label} + + ))} +
+ ) : null} +
+ ); + } + + return ( +
setOpen(true)} + onMouseLeave={() => setOpen(false)} + > + + + {open ? ( +
+ + همه محصولات + + {categories.length > 0 ? ( +
+ ) : null} + {categories.map((category) => ( + + {category.label} + + ))} +
+ ) : null} +
+ ); +} + +export function Header({ + forceSolid = false, + productCategories = [], + logoUrl = null, +}: HeaderProps) { const [open, setOpen] = useState(false); const [scrolled, setScrolled] = useState(forceSolid); + const hasProductDropdown = productCategories.length > 0; useEffect(() => { if (forceSolid) { @@ -29,6 +179,52 @@ export function Header({ forceSolid = false }: HeaderProps) { }; }, [open]); + const renderNavItem = (item: (typeof navItems)[number]) => { + if (item.href === "/user-products" && hasProductDropdown) { + return ( + + ); + } + + return ( + + {item.label} + + ); + }; + + const renderMobileNavItem = (item: (typeof navItems)[number]) => { + if (item.href === "/user-products" && hasProductDropdown) { + return ( + setOpen(false)} + /> + ); + } + + return ( + setOpen(false)} + > + {item.label} + + ); + }; + return (
- - - M - + + {logoUrl ? ( + {site.name} + ) : ( + + M + + )} {site.nameEn} @@ -51,15 +259,7 @@ export function Header({ forceSolid = false }: HeaderProps) {
@@ -84,7 +284,7 @@ export function Header({ forceSolid = false }: HeaderProps) { type="button" aria-label="منو" className="grid h-10 w-10 place-items-center border border-white/25 !text-white lg:hidden" - onClick={() => setOpen((v) => !v)} + onClick={() => setOpen((value) => !value)} > منو @@ -108,16 +308,7 @@ export function Header({ forceSolid = false }: HeaderProps) { {open ? (