Files
mashinify/src/components/ProductTabs.tsx
T
Alireza HassaniandCursor 184d2f1ea4 Enhance product detail page and header with API-driven navigation.
Add gallery lightbox, tabs, marketplace CTAs, and similar products on product pages; load the business logo and level-2 category dropdown in the header.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 20:38:51 +03:30

214 lines
6.7 KiB
TypeScript

"use client";
import { useMemo, useState, type ReactNode } from "react";
import { site } from "@/data/home";
type ProductTabsProps = {
description?: string | null;
technicalNotes?: string | null;
deliveryNotes?: string | null;
technicalValues?: unknown;
};
type TabId = "description" | "technical" | "comments";
const tabMeta: Record<
TabId,
{ label: string; labelEn: string }
> = {
description: { label: "توضیحات", labelEn: "Description" },
technical: { label: "مشخصات فنی", labelEn: "Technical Info" },
comments: { label: "نظرات", labelEn: "Comments" },
};
function renderTechnicalValues(value: unknown): ReactNode {
if (value === null || value === undefined) return null;
if (typeof value === "string") {
return (
<p className="whitespace-pre-line text-base leading-8 text-muted">
{value}
</p>
);
}
if (Array.isArray(value)) {
if (value.length === 0) return null;
return (
<ul className="space-y-2 text-base leading-8 text-muted">
{value.map((item, index) => (
<li key={index} className="flex gap-2">
<span className="text-brand">•</span>
<span>{typeof item === "object" ? JSON.stringify(item) : String(item)}</span>
</li>
))}
</ul>
);
}
if (typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>);
if (entries.length === 0) return null;
return (
<div className="overflow-x-auto">
<table className="w-full min-w-[20rem] border-collapse text-sm">
<tbody>
{entries.map(([key, val]) => (
<tr key={key} className="border-b border-line">
<th className="px-4 py-3 text-right font-bold text-brand-ink">
{key}
</th>
<td className="px-4 py-3 text-muted">
{val === null || val === undefined ? "—" : String(val)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
return <p className="text-base leading-8 text-muted">{String(value)}</p>;
}
export function ProductTabs({
description,
technicalNotes,
deliveryNotes,
technicalValues,
}: ProductTabsProps) {
const tabs = useMemo(() => {
const items: { id: TabId; label: string; labelEn: string }[] = [];
if (description?.trim()) {
items.push({ id: "description", ...tabMeta.description });
}
const hasTechnical =
Boolean(technicalNotes?.trim()) ||
Boolean(deliveryNotes?.trim()) ||
(technicalValues !== null &&
technicalValues !== undefined &&
!(typeof technicalValues === "object" &&
!Array.isArray(technicalValues) &&
Object.keys(technicalValues as object).length === 0) &&
!(Array.isArray(technicalValues) && technicalValues.length === 0));
if (hasTechnical) {
items.push({ id: "technical", ...tabMeta.technical });
}
items.push({ id: "comments", ...tabMeta.comments });
return items;
}, [deliveryNotes, description, technicalNotes, technicalValues]);
const [activeTab, setActiveTab] = useState<TabId>(tabs[0]?.id ?? "comments");
if (tabs.length === 0) return null;
return (
<div className="mt-12 border border-line bg-surface">
<div
role="tablist"
aria-label="جزئیات محصول"
className="flex flex-wrap justify-center gap-1 border-b border-line px-2"
>
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={isActive}
onClick={() => setActiveTab(tab.id)}
className={`relative flex flex-col items-center px-3 py-2 transition ${
isActive
? "text-brand"
: "text-muted hover:text-brand-ink"
}`}
>
<span className="text-sm font-extrabold">{tab.label}</span>
<span
className={`mt-0.5 font-industrial text-[0.58rem] tracking-[0.16em] uppercase ${
isActive ? "text-brand/70" : "text-steel"
}`}
>
{tab.labelEn}
</span>
{isActive ? (
<span className="absolute inset-x-0 bottom-0 h-0.5 bg-brand" />
) : null}
</button>
);
})}
</div>
<div className="p-4 md:p-5">
{activeTab === "description" && description ? (
<div role="tabpanel">
<p className="whitespace-pre-line text-base leading-8 text-muted">
{description}
</p>
</div>
) : null}
{activeTab === "technical" ? (
<div role="tabpanel" className="space-y-6">
{technicalNotes ? (
<div>
<h3 className="font-industrial text-xs tracking-[0.16em] text-brand">
TECHNICAL NOTES
</h3>
<p className="mt-3 whitespace-pre-line text-base leading-8 text-muted">
{technicalNotes}
</p>
</div>
) : null}
{renderTechnicalValues(technicalValues)}
{deliveryNotes ? (
<div>
<h3 className="font-industrial text-xs tracking-[0.16em] text-brand">
DELIVERY
</h3>
<p className="mt-3 whitespace-pre-line text-base leading-8 text-muted">
{deliveryNotes}
</p>
</div>
) : null}
</div>
) : null}
{activeTab === "comments" ? (
<div role="tabpanel" className="text-center">
<div className="mx-auto max-w-md py-3">
<p className="font-industrial text-sm tracking-[0.16em] text-brand">
COMMENTS
</p>
<h3 className="mt-3 text-xl font-extrabold text-brand-ink">
برای ثبت نظر وارد حساب کاربری شوید
</h3>
<p className="mt-3 text-sm leading-7 text-muted">
پس از ورود می‌توانید تجربه خود از این محصول را با سایر خریداران
به اشتراک بگذارید.
</p>
<a
href={site.customerLoginUrl}
target="_blank"
rel="noopener noreferrer"
className="btn-primary mt-6 inline-flex"
>
ورود / ثبت‌نام
</a>
</div>
</div>
) : null}
</div>
</div>
);
}