Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | /**
* PhotoLightbox — full-screen zoomable photo viewer for the event-photos marquee.
*
* Wraps `yet-another-react-lightbox` (MIT) with the Zoom + Counter plugins: tap/click to open,
* pinch / double-tap / scroll to zoom, prev/next arrow buttons, swipe, and keyboard (←/→/Esc)
* all come for free, with a focus trap + body-scroll-lock + ARIA handled by the lib.
*
* Bundle boundary: this file (and the YARL CSS it imports) is loaded LAZILY by
* {@link EventPhotosMarquee} only when a photo is first opened, so it never enters the public
* homepage's critical bundle (no MUI here — Tailwind-only public surface).
*
* Images: the lightbox serves the RAW origin (full resolution). The CDN resize Lambda 503s for
* high-megapixel sources above ~1200px wide (verified on BATbern58 — w=1600 → 503, w=512 thumbnail
* → 200), so requesting a large resized variant here would show broken images. Full-res also gives
* the best zoom quality, and this is an explicit one-at-a-time view (not a perf-critical list), so
* the larger bytes are acceptable. The marquee thumbnails stay CDN-resized (w=512, reliable).
* TODO(infra): once the image-resize Lambda handles large sources at viewer widths, switch back to
* a resized src to cut bytes.
*/
import Lightbox from 'yet-another-react-lightbox';
import Zoom from 'yet-another-react-lightbox/plugins/zoom';
import Counter from 'yet-another-react-lightbox/plugins/counter';
import 'yet-another-react-lightbox/styles.css';
import 'yet-another-react-lightbox/plugins/counter.css';
/** Minimal photo shape (decoupled from the generated EventPhotoResponse). */
export interface PhotoLike {
id: string;
displayUrl: string;
filename?: string | null;
}
interface PhotoLightboxProps {
photos: PhotoLike[];
index: number;
open: boolean;
onClose: () => void;
}
export default function PhotoLightbox({ photos, index, open, onClose }: PhotoLightboxProps) {
const slides = photos.map((photo) => ({
src: photo.displayUrl, // raw origin — see file header (resize Lambda 503s at viewer widths)
alt: photo.filename || 'BATbern event photo',
}));
return (
<Lightbox
open={open}
close={onClose}
index={index}
slides={slides}
plugins={[Zoom, Counter]}
// Loop only makes sense with >1 photo; harmless otherwise.
carousel={{ finite: photos.length <= 1 }}
zoom={{ maxZoomPixelRatio: 3, doubleTapDelay: 250 }}
/>
);
}
|