126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { createPortal } from 'react-dom'
|
|
import { ChevronLeft, ChevronRight, X } from 'lucide-react'
|
|
import styles from './ImageLightbox.module.css'
|
|
|
|
interface ImageLightboxProps {
|
|
open: boolean
|
|
images: string[]
|
|
initialIndex?: number
|
|
alt?: string
|
|
onClose: () => void
|
|
}
|
|
|
|
export function ImageLightbox({
|
|
open,
|
|
images,
|
|
initialIndex = 0,
|
|
alt = 'Product image',
|
|
onClose,
|
|
}: ImageLightboxProps) {
|
|
const [index, setIndex] = useState(initialIndex)
|
|
const [mounted, setMounted] = useState(open)
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setMounted(true)
|
|
setIndex(initialIndex)
|
|
}
|
|
}, [open, initialIndex])
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
|
|
const onKey = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') onClose()
|
|
if (event.key === 'ArrowLeft') setIndex((current) => Math.max(0, current - 1))
|
|
if (event.key === 'ArrowRight') {
|
|
setIndex((current) => Math.min(images.length - 1, current + 1))
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', onKey)
|
|
document.body.style.overflow = 'hidden'
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', onKey)
|
|
document.body.style.overflow = ''
|
|
}
|
|
}, [open, images.length, onClose])
|
|
|
|
useEffect(() => {
|
|
if (!mounted || open) return
|
|
const timer = setTimeout(() => setMounted(false), 200)
|
|
return () => clearTimeout(timer)
|
|
}, [mounted, open])
|
|
|
|
if (!mounted || images.length === 0) return null
|
|
|
|
const current = images[index] ?? images[0]
|
|
const hasPrev = index > 0
|
|
const hasNext = index < images.length - 1
|
|
|
|
return createPortal(
|
|
<div
|
|
className={`${styles.overlay} ${open ? styles.overlayIn : styles.overlayOut}`}
|
|
onClick={onClose}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Product image gallery"
|
|
>
|
|
<button type="button" className={styles.closeBtn} onClick={onClose} aria-label="Close gallery">
|
|
<X size={22} />
|
|
</button>
|
|
|
|
<div className={styles.content} onClick={(event) => event.stopPropagation()}>
|
|
{hasPrev && (
|
|
<button
|
|
type="button"
|
|
className={`${styles.navBtn} ${styles.navPrev}`}
|
|
onClick={() => setIndex((current) => current - 1)}
|
|
aria-label="Previous image"
|
|
>
|
|
<ChevronLeft size={24} />
|
|
</button>
|
|
)}
|
|
|
|
<figure className={styles.figure}>
|
|
<img src={current} alt={`${alt} ${index + 1}`} className={styles.image} />
|
|
<figcaption className={styles.counter}>
|
|
{index + 1} / {images.length}
|
|
</figcaption>
|
|
</figure>
|
|
|
|
{hasNext && (
|
|
<button
|
|
type="button"
|
|
className={`${styles.navBtn} ${styles.navNext}`}
|
|
onClick={() => setIndex((current) => current + 1)}
|
|
aria-label="Next image"
|
|
>
|
|
<ChevronRight size={24} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{images.length > 1 && (
|
|
<div className={styles.strip} onClick={(event) => event.stopPropagation()}>
|
|
{images.map((src, imageIndex) => (
|
|
<button
|
|
key={`${src}-${imageIndex}`}
|
|
type="button"
|
|
className={`${styles.thumb} ${imageIndex === index ? styles.thumbActive : ''}`}
|
|
onClick={() => setIndex(imageIndex)}
|
|
aria-label={`View image ${imageIndex + 1}`}
|
|
>
|
|
<img src={src} alt="" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
,
|
|
document.body,
|
|
)
|
|
}
|