Files
dashboards/apps/business/src/components/ImageLightbox.tsx
T

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,
)
}