Farsi RTL Next.js 16 storefront for استاد علی عالمی, cloned from the original Meshkee web-builder site and rebuilt with Tailwind 4, live Meshkee API integration (products, store items, portfolios, blog, workshops, comments, cart, customer auth), and standalone production output for the shared-VM deploy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { notFound, redirect } from "next/navigation";
|
|
import { PageHeader } from "@/components/PageHeader";
|
|
import { Gallery } from "@/components/catalog/Gallery";
|
|
import { Tabs } from "@/components/catalog/Tabs";
|
|
import { TechnicalDataTable } from "@/components/catalog/TechnicalDataTable";
|
|
import { CommentsPanel } from "@/components/catalog/CommentsPanel";
|
|
import { EntityCard } from "@/components/catalog/EntityCard";
|
|
import { SimilarRow } from "@/components/catalog/SimilarRow";
|
|
import { AddToCartBar } from "@/components/catalog/AddToCartBar";
|
|
import { getSession } from "@/lib/auth";
|
|
import { safe } from "@/lib/meshkee/client";
|
|
import { getComments } from "@/lib/meshkee/comments";
|
|
import { normalizeGallery, normalizeImage, pickText, slugify } from "@/lib/meshkee/format";
|
|
import { getProductById, getProductTechnicalInfo } from "@/lib/meshkee/products";
|
|
import { getStoreItemByProduct } from "@/lib/meshkee/store-items";
|
|
|
|
type Params = { id: string; slug: string };
|
|
|
|
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
|
const { id } = await params;
|
|
try {
|
|
const { product } = await getProductById(id);
|
|
const name = pickText(product.nameFa, product.name, product.title);
|
|
return { title: product.seoMetaTitle || name, description: product.seoMetaDescription || product.summary || undefined };
|
|
} catch {
|
|
return { title: "فروشگاه" };
|
|
}
|
|
}
|
|
|
|
export default async function StoreItemDetailPage({ params }: { params: Promise<Params> }) {
|
|
const { id, slug } = await params;
|
|
|
|
let detail;
|
|
try {
|
|
detail = await getProductById(id);
|
|
} catch {
|
|
notFound();
|
|
}
|
|
|
|
const { product, relatedProducts = [] } = detail;
|
|
const name = pickText(product.nameFa, product.name, product.title);
|
|
const canonicalSlug = slugify(name);
|
|
if (canonicalSlug && canonicalSlug !== slug) {
|
|
redirect(`/store/${id}/${canonicalSlug}`);
|
|
}
|
|
|
|
const [technicalInfo, comments, session, storeItemResult] = await Promise.all([
|
|
safe(getProductTechnicalInfo(id), null),
|
|
safe(getComments("store_item", id), { items: [] }),
|
|
getSession(),
|
|
safe(getStoreItemByProduct(id), { storeItem: null }),
|
|
]);
|
|
|
|
const images = normalizeGallery(product.gallery ?? product.images ?? []);
|
|
const mainImage = normalizeImage(product.titleImageUrl ?? product.imageUrl);
|
|
const gallery = mainImage && images.length === 0 ? [mainImage] : images;
|
|
const variants = storeItemResult.storeItem?.variants ?? [];
|
|
|
|
return (
|
|
<>
|
|
<PageHeader title={name} current="فروشگاه" />
|
|
<section className="boxed py-12">
|
|
<div className="grid gap-10 md:grid-cols-2">
|
|
<Gallery images={gallery} alt={name} />
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-ink">{name}</h2>
|
|
{product.summary && <p className="mt-4 text-sm leading-8 text-body">{product.summary}</p>}
|
|
<div className="mt-6">
|
|
<AddToCartBar variants={variants} productName={name} image={mainImage ?? gallery[0] ?? null} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-10">
|
|
<Tabs
|
|
tabs={[
|
|
{
|
|
key: "description",
|
|
label: "توضیحات",
|
|
content: product.descriptionHtml ? (
|
|
<div className="prose max-w-none text-sm leading-8 text-body" dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} />
|
|
) : (
|
|
<p className="text-sm leading-8 text-body">{product.description || product.summary || "توضیحاتی ثبت نشده است."}</p>
|
|
),
|
|
},
|
|
{ key: "technical", label: "مشخصات فنی", content: <TechnicalDataTable info={technicalInfo} /> },
|
|
{
|
|
key: "comments",
|
|
label: `نظرات${comments.items.length ? ` (${comments.items.length.toLocaleString("fa-IR")})` : ""}`,
|
|
content: (
|
|
<CommentsPanel
|
|
entityType="store_item"
|
|
entityId={id}
|
|
initialComments={comments.items}
|
|
user={session?.user ?? null}
|
|
token={session?.token ?? null}
|
|
/>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{relatedProducts.length > 0 && (
|
|
<SimilarRow title="محصولات مشابه">
|
|
{relatedProducts.map((related) => {
|
|
const relatedName = pickText(related.nameFa, related.name, related.title);
|
|
return (
|
|
<EntityCard
|
|
key={related.id}
|
|
href={`/store/${related.id}/${slugify(relatedName)}`}
|
|
image={normalizeImage(related.titleImageUrl ?? related.imageUrl)}
|
|
title={relatedName}
|
|
price={related.discountedPrice ?? related.price}
|
|
originalPrice={related.discountedPrice ? related.price : undefined}
|
|
/>
|
|
);
|
|
})}
|
|
</SimilarRow>
|
|
)}
|
|
</>
|
|
);
|
|
}
|