"use client"; import { useEffect, useMemo, useState } from "react"; const ADMIN_TOKEN_KEY = "porthole_admin_token"; type Tag = { id: string; name: string; created_at: string; }; type Album = { id: string; name: string; created_at: string; }; export default function AdminPage() { const [token, setToken] = useState(""); const [tokenInput, setTokenInput] = useState(""); const [tokenMessage, setTokenMessage] = useState(null); const [tags, setTags] = useState([]); const [tagsError, setTagsError] = useState(null); const [tagsLoading, setTagsLoading] = useState(false); const [newTag, setNewTag] = useState(""); const [albums, setAlbums] = useState([]); const [albumsError, setAlbumsError] = useState(null); const [albumsLoading, setAlbumsLoading] = useState(false); const [newAlbum, setNewAlbum] = useState(""); useEffect(() => { if (typeof window === "undefined") return; const stored = sessionStorage.getItem(ADMIN_TOKEN_KEY) ?? ""; setToken(stored); setTokenInput(stored); }, []); const adminHeaders = useMemo(() => { if (!token) return null; return { "X-Porthole-Admin-Token": token }; }, [token]); async function loadTags() { if (!adminHeaders) { setTagsError("Set admin token first."); return; } setTagsLoading(true); setTagsError(null); try { const res = await fetch("/api/tags", { headers: adminHeaders, cache: "no-store", }); if (!res.ok) throw new Error(`tags_fetch_failed:${res.status}`); const json = (await res.json()) as Tag[]; setTags(json); } catch (err) { setTagsError(err instanceof Error ? err.message : String(err)); } finally { setTagsLoading(false); } } async function loadAlbums() { if (!adminHeaders) { setAlbumsError("Set admin token first."); return; } setAlbumsLoading(true); setAlbumsError(null); try { const res = await fetch("/api/albums", { headers: adminHeaders, cache: "no-store", }); if (!res.ok) throw new Error(`albums_fetch_failed:${res.status}`); const json = (await res.json()) as Album[]; setAlbums(json); } catch (err) { setAlbumsError(err instanceof Error ? err.message : String(err)); } finally { setAlbumsLoading(false); } } async function handleSaveToken(event: React.FormEvent) { event.preventDefault(); if (typeof window === "undefined") return; const trimmed = tokenInput.trim(); if (trimmed) { sessionStorage.setItem(ADMIN_TOKEN_KEY, trimmed); setToken(trimmed); setTokenMessage("Token saved for this session."); } else { sessionStorage.removeItem(ADMIN_TOKEN_KEY); setToken(""); setTokenMessage("Token cleared."); } } async function handleCreateTag(event: React.FormEvent) { event.preventDefault(); if (!adminHeaders) { setTagsError("Set admin token first."); return; } if (!newTag.trim()) { setTagsError("Tag name is required."); return; } try { setTagsError(null); const res = await fetch("/api/tags", { method: "POST", headers: { ...adminHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ name: newTag.trim() }), }); if (!res.ok) throw new Error(`tag_create_failed:${res.status}`); setNewTag(""); await loadTags(); } catch (err) { setTagsError(err instanceof Error ? err.message : String(err)); } } async function handleCreateAlbum(event: React.FormEvent) { event.preventDefault(); if (!adminHeaders) { setAlbumsError("Set admin token first."); return; } if (!newAlbum.trim()) { setAlbumsError("Album name is required."); return; } try { setAlbumsError(null); const res = await fetch("/api/albums", { method: "POST", headers: { ...adminHeaders, "Content-Type": "application/json" }, body: JSON.stringify({ name: newAlbum.trim() }), }); if (!res.ok) throw new Error(`album_create_failed:${res.status}`); setNewAlbum(""); await loadAlbums(); } catch (err) { setAlbumsError(err instanceof Error ? err.message : String(err)); } } return (

Admin

Manage tags and albums. Admin token is stored in sessionStorage.

Admin Token

setTokenInput(e.target.value)} style={{ padding: 8, borderRadius: 6, border: "1px solid #ccc" }} />
{tokenMessage ? (
{tokenMessage}
) : null}

Tags

setNewTag(e.target.value)} style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }} />
{tagsError ? (
{tagsError}
) : null}
    {tags.length === 0 ? (
  • No tags yet.
  • ) : ( tags.map((tag) =>
  • {tag.name}
  • ) )}

Albums

setNewAlbum(e.target.value)} style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }} />
{albumsError ? (
{albumsError}
) : null}
    {albums.length === 0 ? (
  • No albums yet.
  • ) : ( albums.map((album) =>
  • {album.name}
  • ) )}
); }