TheKalawati/src/WeddingGallery/LightboxModal.tsx

216 lines
7.2 KiB
TypeScript

// 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<number>(startIndex);
const theme = useTheme();
const touchStartX = useRef<number | null>(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 (
<Dialog
open={open}
onClose={onClose}
fullScreen
PaperProps={{ sx: { backgroundColor: 'rgba(0,0,0,0.92)' } }}
>
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
touchAction: 'pan-y',
}}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
{/* Close */}
<IconButton
onClick={onClose}
sx={{ position: 'absolute', top: 16, right: 16, color: 'white', zIndex: 40 }}
aria-label="Close"
>
<CloseIcon />
</IconButton>
{/* Download */}
<IconButton
sx={{ position: 'absolute', top: 16, right: 72, color: 'white', zIndex: 40 }}
aria-label="Download"
onClick={() => {
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);
}
}}
>
<DownloadIcon />
</IconButton>
{/* Prev */}
<IconButton
onClick={prev}
sx={{
position: 'absolute',
left: 8,
color: 'rgba(255,255,255,0.9)',
zIndex: 40,
bgcolor: 'transparent',
}}
aria-label="Previous"
>
<ArrowBackIosNewIcon />
</IconButton>
{/* Next */}
<IconButton
onClick={next}
sx={{
position: 'absolute',
right: 8,
color: 'rgba(255,255,255,0.9)',
zIndex: 40,
bgcolor: 'transparent',
}}
aria-label="Next"
>
<ArrowForwardIosIcon />
</IconButton>
{/* Image */}
<Box
sx={{
maxWidth: '95%',
maxHeight: '88%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
}}
>
<img
src={photo.url}
alt={photo.title ?? photo.filename}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
borderRadius: 6,
boxShadow: '0 10px 30px rgba(0,0,0,0.6)',
}}
draggable={false}
/>
</Box>
{/* Caption / Position */}
<Box sx={{ position: 'absolute', bottom: 20, left: 20, right: 20, textAlign: 'center', zIndex: 40 }}>
<Typography variant="subtitle1" color="white" noWrap>
{photo.title ?? photo.filename}
</Typography>
<Typography variant="caption" color="rgba(255,255,255,0.8)">
{index + 1} / {photos.length} {photo.created_at ? `${new Date(photo.created_at).toLocaleDateString()}` : ''}
</Typography>
</Box>
</Box>
</Dialog>
);
}