Nothing here yet — check back soon.
)} {photos.length > 0 && visible.length === 0 && (No photos match this filter.
)}import { useCallback, useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' import { api, postDownload } from '../api' import Gallery from '../components/Gallery' import Lightbox from '../components/Lightbox' import SelectionBar from '../components/SelectionBar' import Stars from '../components/Stars' import TagEditor from '../components/TagEditor' import Thumbs, { ACCEPT_GLYPH, REJECT_GLYPH } from '../components/Thumbs' import useCulling from '../useCulling' import useLightbox from '../useLightbox' import useSelection from '../useSelection' const FILTERS = [ { key: 'all', label: 'All' }, { key: 'accept', label: ACCEPT_GLYPH }, { key: 'reject', label: REJECT_GLYPH }, { key: 'undecided', label: 'Undecided' }, ] const matchesFilter = (photo, key) => key === 'all' || (key === 'undecided' ? !photo.my_verdict : photo.my_verdict === key) export default function SharePage() { const { token } = useParams() const [view, setView] = useState(null) const [error, setError] = useState(null) const [password, setPassword] = useState('') const [unlockError, setUnlockError] = useState(null) const [filter, setFilter] = useState('all') const photos = view?.photos ?? [] const visible = useMemo(() => photos.filter((p) => matchesFilter(p, filter)), [photos, filter]) const filterCounts = useMemo(() => { const counts = { all: photos.length, accept: 0, reject: 0, undecided: 0 } for (const p of photos) counts[p.my_verdict ?? 'undecided'] += 1 return counts }, [photos]) // Selection spans the whole album (so switching filters keeps it), while // the selection bar describes only the current view: its counts, bytes and // downloads cover the visible photos, and "select all" adds them. const { selected, toggle, selectAll, clear } = useSelection(photos) const visibleSelected = visible.filter((p) => selected.has(p.id)) const sumBytes = (list) => list.reduce((sum, p) => sum + p.size_bytes, 0) const lightbox = useLightbox(visible) const load = useCallback( () => api(`/api/share/${token}`).then(setView).catch((e) => setError(e.message)), [token], ) useEffect(() => { load() }, [load]) const unlock = async (e) => { e.preventDefault() try { await api(`/api/share/${token}/unlock`, { method: 'POST', body: { password } }) setUnlockError(null) load() } catch (err) { setUnlockError(err.status === 401 ? 'Wrong password' : err.message) } } const patchPhoto = (photoId, patch) => { setView((v) => ({ ...v, photos: v.photos.map((p) => (p.id === photoId ? { ...p, ...patch } : p)), })) } // Optimistic write: patch local state, PUT, reload from the server on // failure to undo the patch. const saveFeedback = async (photo, patch, endpoint, body) => { patchPhoto(photo.id, patch) try { await api(`/api/share/${token}/photos/${photo.id}/${endpoint}`, { method: 'PUT', body }) } catch { load() } } const setRating = (photo, rating) => saveFeedback(photo, { my_rating: rating || null }, 'rating', { rating }) const setVerdict = (photo, verdict) => saveFeedback(photo, { my_verdict: verdict }, 'verdict', { verdict }) const setTags = (photo, tags) => saveFeedback(photo, { my_tags: tags }, 'tags', { tags }) const culling = useCulling({ visible: lightbox.view, lightbox, setVerdict, setRating, toggle, canSelect: !!view?.allow_download, }) if (error) return
{view.album_description}
}{photos.length} photo{photos.length === 1 ? '' : 's'} · tap a photo to view, rate and tag · swipe ↑ to accept, ↓ to reject · ? shows keyboard shortcuts
{photos.length > 0 && (Nothing here yet — check back soon.
)} {photos.length > 0 && visible.length === 0 && (No photos match this filter.
)}