Connect Meshkee APIs and add catalog, blog, and content pages.

Wire user-products and blogs to the Website API, add about/contact/categories routes, and polish flat UI filters and page banners.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 10:54:27 +03:30
co-authored by Cursor
parent e74e5ca45f
commit 8194b7292f
31 changed files with 2160 additions and 204 deletions
+185
View File
@@ -0,0 +1,185 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { SiteShell } from "@/components/SiteShell";
import {
formatCondition,
formatPrice,
getUserProductBySlugOrNull,
productCategory,
productTitle,
} from "@/lib/api/user-products";
import { site } from "@/data/home";
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> {
const { slug } = await params;
const product = await getUserProductBySlugOrNull(slug);
if (!product) return { title: "محصول یافت نشد" };
const title = productTitle(product);
return {
title,
description: product.descriptionFa || product.description || 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 = resolveGallery(product);
const hero = gallery[0] ?? null;
const city = product.cityNameFa || product.cityName || null;
const country = product.countryNameFa || product.countryName || null;
const description =
product.descriptionFa || product.description || product.descriptionEn;
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]">
<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>
<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>
)}
{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">
<a href={`tel:${site.phone}`} className="btn-primary">
تماس برای استعلام
</a>
<Link href="/user-products" className="btn-secondary">
بازگشت به لیست
</Link> </div>
</div>
</div>
</div>
</article>
</SiteShell>
);
}