// DiveDex — Screens B: Recap, Dex, Creature Detail, Team, Achievements, Side Quests, Memories, Story Card, Settings // Pulls a dive's photos out of IndexedDB and renders them as a thumbnail // grid. Self-contained — handles loading, empty, and revocation of the // object URLs on unmount. Used by Recap (single dive) and Memories // (per-day inline strips). // Aggregated trip gallery — pulls photos for the given list of dive ids, // concatenates them newest-dive-first, and renders as a tap-to-zoom grid. // Used on the Memories screen. function TripGallery({ diveIds }) { const [photos, setPhotos] = React.useState(null); const [lightbox, setLightbox] = React.useState(null); React.useEffect(() => { let cancelled = false; if (!window.DD_PHOTOS) { setPhotos([]); return; } Promise.all(diveIds.map(id => window.DD_PHOTOS.getPhotosByDive(id).catch(() => []))) .then(lists => { if (cancelled) return; const flat = []; lists.forEach((list, i) => list.forEach(p => flat.push({ ...p, _order: lists.length - i }))); flat.sort((a, b) => b._order - a._order || (a.createdAt < b.createdAt ? 1 : -1)); setPhotos(flat); }); return () => { cancelled = true; }; }, [diveIds.join('|')]); const urls = React.useMemo(() => (photos || []).map(p => URL.createObjectURL(p.blob)), [photos]); React.useEffect(() => () => urls.forEach(u => URL.revokeObjectURL(u)), [urls]); if (!photos || photos.length === 0) return null; return ( <>