// Passage — UI components. const { useState: uS, useRef: uR, useEffect: uE, useMemo: uM } = React; function MobileHeader({ view, onBack, title, right, leftOverride }) { if (view === "viewer") return null; return (
{leftOverride !== undefined ? leftOverride : ( view === "album" ? ( ) :
)}
{title}
{right}
); } function ProfileStrip({ profileUser, albums, canEdit, onEdit, onLogout }) { const totalPhotos = albums.reduce((n, a) => n + a.photos.length, 0); const countries = new Set(albums.map((a) => a.country).filter(Boolean)).size; const avatar = albums.find((a) => a.coverUrl)?.coverUrl; return (
{!avatar && }
{albums.length}albums
{totalPhotos}photos
{countries}countries
{profileUser.display || profileUser.username}
@{profileUser.username}
{profileUser.bio &&
{profileUser.bio}
} {canEdit && (
)}
); } function TabStrip({ tab, setTab, tabs }) { return ( ); } function AlbumsList({ albums, onOpen, onNew, canEdit }) { if (albums.length === 0) { return (
No albums yet
{canEdit ? "Create your first album to start collecting photos from your trips." : "This gallery is still empty."}
{canEdit && }
); } return (
{albums.map((a, i) => (
onOpen(a.id)}>
{!a.coverUrl && }
{String(i + 1).padStart(2, "0")} {a.title}
{[a.country, [a.month, a.year].filter(Boolean).join(" "), a.photos.length + " photo" + (a.photos.length === 1 ? "" : "s")] .filter(Boolean).join(" · ")}
))}
); } function AllPhotosGrid({ albums, onOpenPhoto, onNew, canEdit }) { const all = []; albums.forEach((a) => a.photos.forEach((p, idx) => all.push({ albumId: a.id, photoIdx: idx, src: p.src, title: a.title }))); if (all.length === 0) { return (
No photos yet
{canEdit ? "Photos you add to any album show up here." : "Nothing here yet."}
{canEdit && }
); } return (
{all.map((p, i) => (
); } function AlbumView({ album, canEdit, onOpenPhoto, onAddPhotos, onDeleteAlbum, onEditAlbum, uploading }) { const inputRef = uR(null); const pick = () => inputRef.current && inputRef.current.click(); const onFiles = (e) => { const files = Array.from(e.target.files || []); if (files.length) onAddPhotos(files); e.target.value = ""; }; return (
{!album.coverUrl &&
}
{[album.country, [album.month, album.year].filter(Boolean).join(" ")].filter(Boolean).join(" · ") || "New album"}

{album.title}

{album.subtitle &&
{album.subtitle}
}
{album.caption &&

{album.caption}

}
{album.photos.length}photos
{album.year || "—"}year
{album.country || "—"}country
{canEdit && (
)} {album.photos.length === 0 ? (
{canEdit ? 'Tap "Add photos" to pick from your Photos library.' : "No photos in this album yet."}
) : (
{album.photos.map((p, i) => (
)}
); } function PhotoViewer({ album, idx, canEdit, onClose, onChange, onDelete }) { const startX = uR(0); const [offset, setOffset] = uS(0); const [dragging, setDragging] = uS(false); uE(() => { const onKey = (e) => { if (e.key === "Escape") onClose(); else if (e.key === "ArrowLeft" && idx > 0) onChange(idx - 1); else if (e.key === "ArrowRight" && idx < album.photos.length - 1) onChange(idx + 1); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [idx, album, onChange, onClose]); if (idx == null || !album) return null; const photo = album.photos[idx]; if (!photo) return null; const onStart = (e) => { const t = e.touches ? e.touches[0] : e; startX.current = t.clientX; setDragging(true); }; const onMove = (e) => { if (!dragging) return; const t = e.touches ? e.touches[0] : e; setOffset(t.clientX - startX.current); }; const onEnd = () => { setDragging(false); if (offset > 60 && idx > 0) onChange(idx - 1); else if (offset < -60 && idx < album.photos.length - 1) onChange(idx + 1); setOffset(0); }; return (
{String(idx + 1).padStart(2, "0")} / {String(album.photos.length).padStart(2, "0")}
{canEdit ? ( ) :
}
{photo.caption
{album.title}
{photo.caption &&
{photo.caption}
}
{album.photos.map((_, i) => ( ))}
); } function AlbumSheet({ open, initial, onClose, onSave, busy }) { const [title, setTitle] = uS(""); const [subtitle, setSubtitle] = uS(""); const [country, setCountry] = uS(""); const [month, setMonth] = uS(""); const [year, setYear] = uS(""); const [caption, setCaption] = uS(""); const [err, setErr] = uS(""); uE(() => { if (!open) return; setTitle(initial?.title || ""); setSubtitle(initial?.subtitle || ""); setCountry(initial?.country || ""); setMonth(initial?.month || ""); setYear(initial?.year ? String(initial.year) : String(new Date().getFullYear())); setCaption(initial?.caption || ""); setErr(""); }, [open, initial]); if (!open) return null; const submit = async (e) => { e && e.preventDefault && e.preventDefault(); if (!title.trim()) return; setErr(""); try { await onSave({ title: title.trim(), subtitle: subtitle.trim(), country: country.trim(), month: month.trim(), year: year ? Number(year) : null, caption: caption.trim(), }); } catch (e) { setErr(e.message || "Error"); } }; return (
e.stopPropagation()} onSubmit={submit}>
{initial ? "Edit album" : "New album"}
{err &&
{err}
}