Files
ali-alemi/src/app/store/[id]/[slug]/page.tsx
T
amirhosein.ashourlooandClaude Sonnet 5 8f102a8250 Initial commit: Ali Alemi storefront (Next.js, Meshkee API integration)
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>
2026-09-16 09:43:10 +03:30

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>
)}
</>
);
}