Polish product/blog detail pages: gallery, tabs, Persian names, image fixes

- 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>
This commit is contained in:
amirhosein.ashourloo
2026-08-18 19:38:54 +03:30
co-authored by Claude Sonnet 5
parent 2b5f4e437b
commit 7fb1f5a60f
11 changed files with 351 additions and 168 deletions
+81
View File
@@ -0,0 +1,81 @@
"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>
);
}