Connect storefront to backend catalog, search, and home slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-27 18:30:52 +03:30
co-authored by Cursor
parent 2f857f1a90
commit 0074ab6629
106 changed files with 5920 additions and 81 deletions
@@ -0,0 +1,194 @@
"use client";
import Image from "next/image";
import { useEffect, useState } from "react";
import type { ProductComment, ProductDetail } from "@/data/product-detail";
type TabId = "technical" | "comments" | "sizing";
const TABS: Array<{ id: TabId; labelFa: string; labelEn: string }> = [
{ id: "technical", labelFa: "مشخصات فنی", labelEn: "Technical info" },
{ id: "comments", labelFa: "نظرات", labelEn: "Comments" },
{ id: "sizing", labelFa: "راهنمای سایز", labelEn: "Sizing guide" },
];
type ProductTabsProps = {
product: ProductDetail;
initialTab?: TabId;
};
function Stars({ rating }: { rating: number }) {
return (
<span dir="ltr" className="font-montserrat text-xs tracking-widest text-ink" aria-label={`${rating} از ۵`}>
{"★".repeat(Math.max(0, Math.min(5, rating)))}
<span className="text-muted">{"★".repeat(Math.max(0, 5 - rating))}</span>
</span>
);
}
function CommentsList({ comments }: { comments: ProductComment[] }) {
if (!comments.length) {
return (
<p className="text-sm text-muted">هنوز نظری ثبت نشده است.</p>
);
}
return (
<ul className="space-y-4">
{comments.map((c) => (
<li key={c.id} className="border border-line bg-white p-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<p className="text-sm text-ink">{c.author}</p>
<Stars rating={c.rating} />
</div>
<p dir="ltr" className="mt-1 font-montserrat text-[10px] text-muted">
{c.date}
</p>
<p className="mt-3 text-sm leading-7 text-ink/80">{c.body}</p>
</li>
))}
</ul>
);
}
export default function ProductTabs({
product,
initialTab = "technical",
}: ProductTabsProps) {
const [tab, setTab] = useState<TabId>(initialTab);
useEffect(() => {
const onHash = () => {
if (window.location.hash === "#product-tabs") {
setTab("sizing");
}
};
onHash();
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
return (
<section id="product-tabs" className="scroll-mt-28 border-t border-line pt-10">
<div className="flex flex-wrap gap-2 border-b border-line pb-px">
{TABS.map((t) => {
const active = tab === t.id;
return (
<button
key={t.id}
type="button"
onClick={() => setTab(t.id)}
className={`-mb-px border-b-2 px-3 py-3 text-start transition-colors sm:px-4 ${
active
? "border-ink text-ink"
: "border-transparent text-muted hover:text-ink"
}`}
aria-selected={active}
role="tab"
>
<span className="block text-sm sm:text-base">{t.labelFa}</span>
<span
dir="ltr"
className="mt-0.5 block font-montserrat text-[10px] tracking-[0.12em] uppercase opacity-70"
>
{t.labelEn}
</span>
</button>
);
})}
</div>
<div className="py-8" role="tabpanel">
{tab === "technical" ? (
<div className="space-y-6">
<p className="max-w-3xl text-sm leading-7 text-ink/75">
{product.description}
</p>
<dl className="divide-y divide-line border border-line">
{product.technical.map((row) => (
<div
key={row.labelEn}
className="grid grid-cols-1 gap-1 px-4 py-3 sm:grid-cols-[minmax(0,11rem)_1fr] sm:items-baseline sm:gap-6"
>
<dt>
<span className="text-sm text-ink">{row.labelFa}</span>
<span
dir="ltr"
className="mt-0.5 block font-montserrat text-[10px] tracking-wide text-muted"
>
{row.labelEn}
</span>
</dt>
<dd className="text-sm text-ink/80 sm:text-end">{row.value}</dd>
</div>
))}
</dl>
</div>
) : null}
{tab === "comments" ? (
<CommentsList comments={product.comments} />
) : null}
{tab === "sizing" ? (
<div className="grid gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] lg:items-start">
<div className="relative aspect-[4/5] overflow-hidden rounded-[5px] border border-line bg-[#f6f6f4]">
{product.sizeGuideImage ? (
<Image
src={product.sizeGuideImage}
alt="راهنمای سایز"
fill
sizes="(max-width: 1024px) 100vw, 520px"
className="object-cover object-top"
/>
) : (
<span className="absolute inset-0 flex items-center justify-center text-sm text-muted">
Size guide image
</span>
)}
</div>
<div>
<h3 className="font-doran text-xl text-ink">جدول اندازه‌ها</h3>
<p
dir="ltr"
className="mt-1 font-montserrat text-[10px] uppercase tracking-[0.16em] text-muted"
>
Measurements (cm)
</p>
<p className="mt-3 text-sm leading-7 text-ink/75">
{product.sizeGuideNote}
</p>
<div className="mt-5 overflow-x-auto border border-line">
<table className="w-full min-w-[320px] text-sm">
<thead className="bg-[#f7f7f5] text-muted">
<tr>
<th className="px-3 py-2.5 text-start font-normal">سایز</th>
<th className="px-3 py-2.5 text-start font-normal">سینه</th>
<th className="px-3 py-2.5 text-start font-normal">کمر</th>
<th className="px-3 py-2.5 text-start font-normal">قد</th>
</tr>
</thead>
<tbody>
{[
["S", "۸۸", "۷۰", "۹۲"],
["M", "۹۲", "۷۴", "۹۵"],
["L", "۹۶", "۷۸", "۹۸"],
["XL", "۱۰۰", "۸۲", "۱۰۰"],
].map((row) => (
<tr key={row[0]} className="border-t border-line">
{row.map((cell) => (
<td key={cell} className="px-3 py-2.5 text-ink">
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
) : null}
</div>
</section>
);
}