// LightboxModal.tsx import React, { useEffect, useRef, useState } from 'react'; import { Dialog, IconButton, Box, Typography, useTheme, } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import DownloadIcon from '@mui/icons-material/Download'; export type LightboxPhoto = { id: number | string; url: string; title?: string; filename?: string; created_at?: string; }; type Props = { open: boolean; photos: LightboxPhoto[]; startIndex?: number; onClose: () => void; startIndexCallback?: (idx: number) => void; // optional: inform parent when slide changes }; const SWIPE_THRESHOLD = 40; // pixels export default function LightboxModal({ open, photos, startIndex = 0, onClose, startIndexCallback }: Props) { const [index, setIndex] = useState(startIndex); const theme = useTheme(); const touchStartX = useRef(null); const touchDeltaX = useRef(0); useEffect(() => { setIndex(startIndex); }, [startIndex, open]); useEffect(() => { function onKey(e: KeyboardEvent) { if (!open) return; if (e.key === 'Escape') onClose(); if (e.key === 'ArrowLeft') prev(); if (e.key === 'ArrowRight') next(); } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [open, index]); useEffect(() => { // inform parent if needed startIndexCallback?.(index); }, [index]); const prev = () => setIndex((i) => (i - 1 + photos.length) % photos.length); const next = () => setIndex((i) => (i + 1) % photos.length); // Preload neighbors useEffect(() => { if (!photos || photos.length === 0) return; const prevIdx = (index - 1 + photos.length) % photos.length; const nextIdx = (index + 1) % photos.length; const p1 = new Image(); p1.src = photos[prevIdx]?.url; const p2 = new Image(); p2.src = photos[nextIdx]?.url; }, [index, photos]); // Touch handlers for swipe const onTouchStart = (e: React.TouchEvent) => { touchDeltaX.current = 0; touchStartX.current = e.touches?.[0]?.clientX ?? null; }; const onTouchMove = (e: React.TouchEvent) => { if (touchStartX.current == null) return; const x = e.touches?.[0]?.clientX ?? 0; touchDeltaX.current = x - (touchStartX.current ?? 0); }; const onTouchEnd = () => { const delta = touchDeltaX.current; if (Math.abs(delta) > SWIPE_THRESHOLD) { if (delta < 0) next(); else prev(); } touchStartX.current = null; touchDeltaX.current = 0; }; if (!photos || photos.length === 0) return null; const photo = photos[index]; return ( {/* Close */} {/* Download */} { try { const a = document.createElement('a'); a.href = photo.url; a.download = `${photo.filename ?? photo.title ?? 'image'}`; document.body.appendChild(a); a.click(); a.remove(); } catch (err) { console.error('download', err); } }} > {/* Prev */} {/* Next */} {/* Image */} {photo.title {/* Caption / Position */} {photo.title ?? photo.filename} {index + 1} / {photos.length} {photo.created_at ? `• ${new Date(photo.created_at).toLocaleDateString()}` : ''} ); }