const { useState: mS, useEffect: mE, useMemo: mM, useCallback: mC } = React; // ─── Router ─────────────────────────────────────────────────── function parseRoute(pathname) { const p = pathname.replace(/\/+$/, "") || "/"; if (p === "/login") return { name: "login" }; if (p === "/signup") return { name: "signup" }; const m = p.match(/^\/u\/([^/]+)$/); if (m) return { name: "profile", username: decodeURIComponent(m[1]) }; return { name: "index" }; } function navigate(to) { window.history.pushState({}, "", to); window.dispatchEvent(new PopStateEvent("popstate")); } // ─── Photos shape helper ───────────────────────────────────── function normalizeAlbums(raw) { return raw.map((a) => { const photos = (a.photos || []).map((p) => ({ ...p, src: p.url })); const coverUrl = a.coverPhotoId ? (photos.find((p) => p.id === a.coverPhotoId)?.src || photos[0]?.src || null) : (photos[0]?.src || null); return { ...a, photos, coverUrl }; }); } // ─── Top-level app ──────────────────────────────────────────── function PassageRoot() { const [route, setRoute] = mS(() => parseRoute(window.location.pathname)); const [me, setMe] = mS(undefined); // undefined = loading, null = logged out, object = logged in mE(() => { const onPop = () => setRoute(parseRoute(window.location.pathname)); window.addEventListener("popstate", onPop); return () => window.removeEventListener("popstate", onPop); }, []); mE(() => { API.me().then(setMe).catch(() => setMe(null)); }, []); if (me === undefined) return ; // Index route: send logged-in users to their gallery, else to login. if (route.name === "index") { if (me) { navigate("/u/" + me.username); return ; } return { setMe(u); navigate("/u/" + u.username); }} onSwitch={(m) => navigate("/" + m)} />; } if (route.name === "login") return { setMe(u); navigate("/u/" + u.username); }} onSwitch={(m) => navigate("/" + m)} />; if (route.name === "signup") return { setMe(u); navigate("/u/" + u.username); }} onSwitch={(m) => navigate("/" + m)} />; if (route.name === "profile") { return ( ); } return ; } function Splash() { return ( ◐ ); } // ─── Profile page ───────────────────────────────────────────── function ProfilePage({ username, me, setMe }) { const [profileUser, setProfileUser] = mS(null); const [albums, setAlbums] = mS([]); const [loading, setLoading] = mS(true); const [notFound, setNotFound] = mS(false); const [view, setView] = mS({ kind: "list", tab: "albums" }); const [viewer, setViewer] = mS(null); const [sheet, setSheet] = mS(null); // null | {kind:'new'|'edit', album?} const [profileSheet, setProfileSheet] = mS(false); const [busy, setBusy] = mS(false); const [uploading, setUploading] = mS(false); const canEdit = !!(me && profileUser && me.username === profileUser.username); async function refresh() { setLoading(true); try { const data = await API.profile(username); setProfileUser(data.user); setAlbums(normalizeAlbums(data.albums)); setNotFound(false); } catch (e) { setNotFound(true); } finally { setLoading(false); } } mE(() => { refresh(); /* eslint-disable-next-line */ }, [username]); if (loading && !profileUser) return ; if (notFound) return ( ◐} /> ◐ No user @{username} This gallery doesn't exist. navigate("/")}>Back ); const openAlbum = (id) => { setView({ kind: "album", id }); window.scrollTo(0, 0); }; const back = () => { setView({ kind: "list", tab: "albums" }); window.scrollTo(0, 0); }; const setTab = (t) => setView({ kind: "list", tab: t }); const openPhoto = (albumId, idx) => setViewer({ albumId, idx }); const closePhoto = () => setViewer(null); const viewerAlbum = viewer ? albums.find((a) => a.id === viewer.albumId) : null; const currentAlbum = view.kind === "album" ? albums.find((a) => a.id === view.id) : null; async function createAlbum(data) { setBusy(true); try { const a = await API.createAlbum(data); const fresh = normalizeAlbums([a]); setAlbums((prev) => [...fresh, ...prev]); setSheet(null); openAlbum(a.id); } finally { setBusy(false); } } async function editAlbum(data) { if (!currentAlbum) return; setBusy(true); try { const a = await API.updateAlbum(currentAlbum.id, data); setAlbums((prev) => prev.map((x) => x.id === a.id ? normalizeAlbums([{ ...a, photos: currentAlbum.photos.map((p) => ({ ...p, url: p.src })) }])[0] : x)); setSheet(null); } finally { setBusy(false); } } async function addPhotos(albumId, files) { setUploading(true); try { const newPhotos = await API.uploadPhotos(albumId, files); const mapped = newPhotos.map((p) => ({ ...p, src: p.url })); setAlbums((prev) => prev.map((a) => { if (a.id !== albumId) return a; const photos = [...a.photos, ...mapped]; const coverUrl = a.coverUrl || photos[0]?.src || null; const coverPhotoId = a.coverPhotoId || photos[0]?.id || null; return { ...a, photos, coverUrl, coverPhotoId }; })); } catch (e) { alert("Upload failed: " + (e.message || e)); } finally { setUploading(false); } } async function deletePhoto(albumId, idx) { const album = albums.find((a) => a.id === albumId); if (!album) return; const photo = album.photos[idx]; if (!photo || !confirm("Delete this photo?")) return; await API.deletePhoto(photo.id); setAlbums((prev) => prev.map((a) => { if (a.id !== albumId) return a; const photos = a.photos.filter((p) => p.id !== photo.id); const wasCover = a.coverPhotoId === photo.id; const coverPhotoId = wasCover ? (photos[0]?.id || null) : a.coverPhotoId; const coverUrl = coverPhotoId ? (photos.find((p) => p.id === coverPhotoId)?.src || null) : null; return { ...a, photos, coverPhotoId, coverUrl }; })); setViewer((v) => { if (!v || v.albumId !== albumId) return v; const nextLen = album.photos.length - 1; if (nextLen <= 0) return null; return { ...v, idx: Math.min(v.idx, nextLen - 1) }; }); } async function deleteAlbum(id) { if (!confirm("Delete this album and all its photos?")) return; await API.deleteAlbum(id); setAlbums((prev) => prev.filter((a) => a.id !== id)); back(); } async function saveProfile(patch) { setBusy(true); try { const u = await API.updateMe(patch); setMe(u); setProfileUser(u); setProfileSheet(false); } finally { setBusy(false); } } async function logout() { await API.logout(); setMe(null); navigate("/login"); } const title = view.kind === "album" ? (currentAlbum?.title || "Album") : `@${profileUser.username}`; const right = ( {canEdit && view.kind === "list" && ( setSheet({ kind: "new" })}> )} {!canEdit && !me && ( navigate("/login")}>Log in )} ); return ( {view.kind === "list" && ( <> setProfileSheet(true)} onLogout={logout} /> {view.tab === "albums" && setSheet({ kind: "new" })} canEdit={canEdit} />} {view.tab === "grid" && setSheet({ kind: "new" })} canEdit={canEdit} />} > )} {view.kind === "album" && currentAlbum && ( addPhotos(currentAlbum.id, files)} onDeleteAlbum={() => deleteAlbum(currentAlbum.id)} onEditAlbum={() => setSheet({ kind: "edit", album: currentAlbum })} uploading={uploading} /> )} {viewer && viewerAlbum && ( setViewer({ ...viewer, idx: i })} onDelete={(i) => deletePhoto(viewer.albumId, i)} /> )} {canEdit && view.kind === "list" && albums.length > 0 && ( setSheet({ kind: "new" })} label="New album" /> )} {sheet && ( setSheet(null)} onSave={sheet.kind === "edit" ? editAlbum : createAlbum} busy={busy} /> )} {profileSheet && ( setProfileSheet(false)} onSave={saveProfile} busy={busy} /> )} ); } ReactDOM.createRoot(document.getElementById("root")).render();