207 lines
7.4 KiB
TypeScript
207 lines
7.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
function CloseIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
|
|
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function Lightbox({
|
|
images,
|
|
alt,
|
|
index,
|
|
onIndexChange,
|
|
onClose,
|
|
}: {
|
|
images: string[];
|
|
alt: string;
|
|
index: number;
|
|
onIndexChange: (i: number) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const hasMultiple = images.length > 1;
|
|
|
|
useEffect(() => {
|
|
document.body.style.overflow = "hidden";
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") onClose();
|
|
if (event.key === "ArrowLeft") onIndexChange((index + 1) % images.length);
|
|
if (event.key === "ArrowRight") onIndexChange((index - 1 + images.length) % images.length);
|
|
};
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => {
|
|
document.body.style.overflow = "";
|
|
document.removeEventListener("keydown", onKeyDown);
|
|
};
|
|
}, [index, images.length, onClose, onIndexChange]);
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-[120] flex flex-col bg-black/95" onClick={onClose}>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="بستن"
|
|
className="absolute top-4 left-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
|
>
|
|
<CloseIcon className="h-6 w-6" />
|
|
</button>
|
|
|
|
<div className="relative flex flex-1 items-center justify-center p-6" onClick={(e) => e.stopPropagation()}>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={images[index]} alt={alt} className="max-h-full max-w-full object-contain" />
|
|
|
|
{hasMultiple && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => onIndexChange((index - 1 + images.length) % images.length)}
|
|
aria-label="تصویر بعدی"
|
|
className="absolute top-1/2 right-4 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
|
>
|
|
<ChevronIcon direction="next" className="h-6 w-6" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => onIndexChange((index + 1) % images.length)}
|
|
aria-label="تصویر قبلی"
|
|
className="absolute top-1/2 left-4 flex h-11 w-11 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
|
>
|
|
<ChevronIcon direction="prev" className="h-6 w-6" />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{hasMultiple && (
|
|
<div
|
|
className="flex justify-center gap-2 overflow-x-auto p-4"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{images.map((img, i) => (
|
|
<button
|
|
key={img + i}
|
|
type="button"
|
|
onClick={() => onIndexChange(i)}
|
|
className={`h-14 w-14 shrink-0 overflow-hidden rounded-lg border-2 transition-colors ${
|
|
i === index ? "border-red" : "border-white/20"
|
|
}`}
|
|
>
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={img} alt="" className="h-full w-full object-cover" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|
|
|
|
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 [lightboxOpen, setLightboxOpen] = useState(false);
|
|
|
|
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}
|
|
onClick={() => setLightboxOpen(true)}
|
|
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-in" : "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="next" 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="prev" 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>
|
|
)}
|
|
|
|
{lightboxOpen && (
|
|
<Lightbox
|
|
images={list}
|
|
alt={alt}
|
|
index={index}
|
|
onIndexChange={setIndex}
|
|
onClose={() => setLightboxOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|