195 lines
6.9 KiB
TypeScript
195 lines
6.9 KiB
TypeScript
"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>
|
||
);
|
||
}
|