Build the RTL Next.js experience with live catalog, journal, customer interactions, and SEO-ready canonical routes. Co-authored-by: Cursor <cursoragent@cursor.com>
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { ChevronLeft, ChevronRight, Expand, X } from "lucide-react";
|
|
import { useEffect, useState, type MouseEvent } from "react";
|
|
|
|
export function ProductGallery({
|
|
images,
|
|
alt,
|
|
}: {
|
|
images: string[];
|
|
alt: string;
|
|
}) {
|
|
const [active, setActive] = useState(0);
|
|
const [lightbox, setLightbox] = useState(false);
|
|
const [origin, setOrigin] = useState("50% 50%");
|
|
|
|
const previous = () => setActive((value) => (value - 1 + images.length) % images.length);
|
|
const next = () => setActive((value) => (value + 1) % images.length);
|
|
|
|
useEffect(() => {
|
|
if (!lightbox) return;
|
|
const onKey = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape") setLightbox(false);
|
|
if (event.key === "ArrowLeft") next();
|
|
if (event.key === "ArrowRight") previous();
|
|
};
|
|
document.body.style.overflow = "hidden";
|
|
window.addEventListener("keydown", onKey);
|
|
return () => {
|
|
document.body.style.overflow = "";
|
|
window.removeEventListener("keydown", onKey);
|
|
};
|
|
}, [lightbox, images.length]);
|
|
|
|
function updateZoomOrigin(event: MouseEvent<HTMLButtonElement>) {
|
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
const x = ((event.clientX - bounds.left) / bounds.width) * 100;
|
|
const y = ((event.clientY - bounds.top) / bounds.height) * 100;
|
|
setOrigin(`${x}% ${y}%`);
|
|
}
|
|
|
|
if (images.length === 0) return null;
|
|
|
|
return (
|
|
<>
|
|
<div className="product-gallery">
|
|
<button
|
|
className="product-gallery-main"
|
|
type="button"
|
|
onClick={() => setLightbox(true)}
|
|
onMouseMove={updateZoomOrigin}
|
|
onMouseLeave={() => setOrigin("50% 50%")}
|
|
aria-label="نمایش تصویر بزرگ"
|
|
>
|
|
<Image
|
|
src={images[active]}
|
|
alt={`${alt} - تصویر ${active + 1}`}
|
|
fill
|
|
loading="eager"
|
|
sizes="(max-width: 900px) 100vw, 55vw"
|
|
style={{ transformOrigin: origin }}
|
|
/>
|
|
<span><Expand /></span>
|
|
</button>
|
|
|
|
{images.length > 1 && (
|
|
<div className="gallery-thumbnails">
|
|
{images.map((image, index) => (
|
|
<button
|
|
key={`${image}-${index}`}
|
|
type="button"
|
|
className={index === active ? "active" : ""}
|
|
onClick={() => setActive(index)}
|
|
aria-label={`تصویر ${index + 1}`}
|
|
>
|
|
<Image
|
|
src={image}
|
|
alt={`${alt} - تصویر کوچک ${index + 1}`}
|
|
fill
|
|
sizes="90px"
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{lightbox && (
|
|
<div className="lightbox" role="dialog" aria-modal="true" aria-label={`گالری ${alt}`}>
|
|
<button className="lightbox-close" type="button" onClick={() => setLightbox(false)} aria-label="بستن">
|
|
<X />
|
|
</button>
|
|
{images.length > 1 && (
|
|
<>
|
|
<button className="lightbox-prev" type="button" onClick={previous} aria-label="تصویر قبلی">
|
|
<ChevronRight />
|
|
</button>
|
|
<button className="lightbox-next" type="button" onClick={next} aria-label="تصویر بعدی">
|
|
<ChevronLeft />
|
|
</button>
|
|
</>
|
|
)}
|
|
<div className="lightbox-image">
|
|
<Image src={images[active]} alt={`${alt} - تصویر بزرگ`} fill sizes="100vw" />
|
|
</div>
|
|
<span className="lightbox-count">{active + 1} / {images.length}</span>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|