Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user