- Product detail: interactive image gallery (thumbnails, prev/next,
hover-zoom), tabbed spec/review/comments section matching the site's
standard pattern, variations/options display, and an availability
label instead of duplicating the contact-store message.
- Fix product.images normalization — the API returns {url} media objects,
not plain strings, so galleries were silently rendering empty.
- Prefer the Persian name (nameFa/productNameFa) everywhere a product
name is shown.
- Product cards: padded, rounded images instead of edge-to-edge with a
dashed divider.
- Blog: wire the homepage carousel to real posts (was static sample
data), drop the detail page's separate cover image, and make images
embedded in article/description HTML fill their container with
rounded corners.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import type { TechnicalInfoField } from "@/lib/meshkee";
|
|
|
|
type Tab = "specs" | "review" | "comments";
|
|
|
|
const TABS: { id: Tab; label: string }[] = [
|
|
{ id: "specs", label: "مشخصات فنی" },
|
|
{ id: "review", label: "نقد و بررسی" },
|
|
{ id: "comments", label: "نظرات کاربران" },
|
|
];
|
|
|
|
export function ProductTabs({
|
|
technicalInfo,
|
|
descriptionHtml,
|
|
}: {
|
|
technicalInfo: TechnicalInfoField[];
|
|
descriptionHtml?: string;
|
|
}) {
|
|
const [active, setActive] = useState<Tab>("specs");
|
|
const activeLabel = TABS.find((t) => t.id === active)?.label ?? "";
|
|
|
|
return (
|
|
<div className="mt-10">
|
|
<div className="flex items-center gap-8 border-b border-border">
|
|
{TABS.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
type="button"
|
|
onClick={() => setActive(tab.id)}
|
|
className={`relative pb-4 text-sm transition-colors ${
|
|
active === tab.id ? "font-bold text-foreground" : "text-muted hover:text-red"
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
{active === tab.id && (
|
|
<span className="absolute inset-x-0 -bottom-px h-0.5 bg-foreground" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="pt-8">
|
|
<h2 className="mb-5 flex items-center gap-2 text-lg font-bold">
|
|
<span className="h-2 w-2 shrink-0 rounded-full bg-red" aria-hidden />
|
|
{activeLabel}
|
|
</h2>
|
|
|
|
{active === "specs" &&
|
|
(technicalInfo.length === 0 ? (
|
|
<EmptyState message="هنوز مشخصات فنیای برای این محصول ثبت نشده است." />
|
|
) : (
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
{technicalInfo.map((field) => (
|
|
<div key={field.label} className="rounded-xl bg-background p-4">
|
|
<p className="mb-1 text-sm font-bold">{field.label} :</p>
|
|
<p className="text-sm text-muted">{field.value}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
|
|
{active === "review" &&
|
|
(descriptionHtml ? (
|
|
<div
|
|
className="prose prose-sm max-w-none"
|
|
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
|
|
/>
|
|
) : (
|
|
<EmptyState message="هنوز نقد و بررسیای برای این محصول ثبت نشده است." />
|
|
))}
|
|
|
|
{active === "comments" && (
|
|
<EmptyState message="هنوز نظری برای این محصول ثبت نشده است." />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|