Create Glassmond storefront with Meshkee integration
Build the RTL Next.js experience with live catalog, journal, customer interactions, and SEO-ready canonical routes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user