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:
co-authored by
Claude Sonnet 5
parent
2b5f4e437b
commit
7fb1f5a60f
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
function ChevronIcon({ direction, className }: { direction: "prev" | "next"; className?: string }) {
|
||||
const d = direction === "prev" ? "M14.5 6.5 9 12l5.5 5.5" : "M9.5 6.5 15 12l-5.5 5.5";
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
|
||||
<path d={d} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductGallery({ images, alt }: { images: string[]; alt: string }) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [zoomed, setZoomed] = useState(false);
|
||||
const [origin, setOrigin] = useState({ x: 50, y: 50 });
|
||||
|
||||
const list = images.length > 0 ? images : ["/images/placeholder.svg"];
|
||||
const hasMultiple = list.length > 1;
|
||||
|
||||
const go = (delta: number) => {
|
||||
setIndex((prev) => (prev + delta + list.length) % list.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
<div
|
||||
className="relative aspect-square w-full overflow-hidden rounded-2xl border border-border bg-surface"
|
||||
onMouseMove={(event) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
setOrigin({
|
||||
x: ((event.clientX - rect.left) / rect.width) * 100,
|
||||
y: ((event.clientY - rect.top) / rect.height) * 100,
|
||||
});
|
||||
}}
|
||||
onMouseEnter={() => setZoomed(true)}
|
||||
onMouseLeave={() => setZoomed(false)}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={list[index]}
|
||||
alt={alt}
|
||||
className="h-full w-full object-contain transition-transform duration-200 ease-out"
|
||||
style={{
|
||||
transform: zoomed ? "scale(2)" : "scale(1)",
|
||||
transformOrigin: `${origin.x}% ${origin.y}%`,
|
||||
cursor: zoomed ? "zoom-out" : "zoom-in",
|
||||
}}
|
||||
/>
|
||||
|
||||
{hasMultiple && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(-1)}
|
||||
aria-label="تصویر بعدی"
|
||||
className="absolute top-1/2 right-3 z-10 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full border border-border bg-surface/90 text-foreground shadow-sm transition-colors hover:border-red hover:text-red"
|
||||
>
|
||||
<ChevronIcon direction="prev" className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(1)}
|
||||
aria-label="تصویر قبلی"
|
||||
className="absolute top-1/2 left-3 z-10 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full border border-border bg-surface/90 text-foreground shadow-sm transition-colors hover:border-red hover:text-red"
|
||||
>
|
||||
<ChevronIcon direction="next" className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasMultiple && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{list.map((img, i) => (
|
||||
<button
|
||||
key={img + i}
|
||||
type="button"
|
||||
onClick={() => setIndex(i)}
|
||||
aria-label={`تصویر ${i + 1}`}
|
||||
className={`h-16 w-16 shrink-0 overflow-hidden rounded-lg border-2 transition-colors ${
|
||||
i === index ? "border-red" : "border-border hover:border-red-muted"
|
||||
}`}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={img} alt="" className="h-full w-full object-cover" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user