// Gallery page for Little Bloom Photo Studio
const { useState: useStateGal, useEffect: useEffectGal, useRef: useRefGal, useCallback: useCallbackGal } = React;
function GalleryPage({ setPage }) {
const [group, setGroup] = useStateGal('all');
const [lightbox, setLightbox] = useStateGal(null); // index into `photos`, or null
const photos = group === 'all'
? STUDIO_GALLERY
: STUDIO_GALLERY.filter(p => p.group === group);
// Changing filter invalidates any open lightbox index
const pickGroup = (id) => { setLightbox(null); setGroup(id); };
return (
{lightbox !== null && (
setLightbox(null)} />
)}
);
}
/* ========== Hero ========== */
function GalleryHero({ count }) {
return (
The Gallery
Every corner of
the studio
A closer look at the space — the daybed, the couch, the bassinet, and every
prop and detail waiting for your next session.
{count} Photographs
);
}
/* ========== Filter pills ========== */
function GalleryFilters({ group, setGroup }) {
return (
{GALLERY_GROUPS.map(g => {
const on = g.id === group;
return (
);
})}
);
}
/* ========== Masonry grid ========== */
function GalleryGrid({ photos, onOpen }) {
return (
{photos.map((p, i) => (
))}
);
}
/* ========== Lightbox ========== */
function Lightbox({ photos, index, setIndex, onClose }) {
const photo = photos[index];
const touchX = useRefGal(null);
const closeRef = useRefGal(null);
const prev = useCallbackGal(() => setIndex(i => (i - 1 + photos.length) % photos.length), [photos.length, setIndex]);
const next = useCallbackGal(() => setIndex(i => (i + 1) % photos.length), [photos.length, setIndex]);
// Keyboard: Esc closes, arrows navigate
useEffectGal(() => {
const onKey = (e) => {
if (e.key === 'Escape') onClose();
else if (e.key === 'ArrowLeft') prev();
else if (e.key === 'ArrowRight') next();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [prev, next, onClose]);
// Lock body scroll while open, compensating for the scrollbar so the
// page behind doesn't jump sideways
useEffectGal(() => {
const { overflow, paddingRight } = document.body.style;
const gap = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = 'hidden';
if (gap > 0) document.body.style.paddingRight = gap + 'px';
return () => {
document.body.style.overflow = overflow;
document.body.style.paddingRight = paddingRight;
};
}, []);
// Move focus into the dialog so keyboard users land in the right place
useEffectGal(() => { closeRef.current && closeRef.current.focus(); }, []);
// Warm the neighbouring full-size images so arrowing through is instant
useEffectGal(() => {
[index - 1, index + 1].forEach(i => {
const p = photos[(i + photos.length) % photos.length];
if (p) { const img = new Image(); img.src = p.src; }
});
}, [index, photos]);
const onTouchStart = (e) => { touchX.current = e.changedTouches[0].clientX; };
const onTouchEnd = (e) => {
if (touchX.current === null) return;
const dx = e.changedTouches[0].clientX - touchX.current;
if (Math.abs(dx) > 50) (dx < 0 ? next : prev)();
touchX.current = null;
};
return (
{/* Top bar */}
e.stopPropagation()}>
{photo.label} · {index + 1} / {photos.length}
{/* Stage */}
e.stopPropagation()}>
e.stopPropagation()}>
Use ← → to browse · Esc to close
Swipe to browse · Tap outside to close
);
}
/* ========== Closing CTA ========== */
function GalleryCTA({ setPage }) {
return (
Come see it in person
Photos only go so far — book the space.
Every prop, backdrop, and piece of furniture you see here is included with
your studio time. Reserve your hours and make it yours.
);
}
window.GalleryPage = GalleryPage;