Files
porthole/apps/web/app/admin/page.tsx
2026-02-02 19:46:24 -08:00

254 lines
7.8 KiB
TypeScript

"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<string | null>(null);
const [tags, setTags] = useState<Tag[]>([]);
const [tagsError, setTagsError] = useState<string | null>(null);
const [tagsLoading, setTagsLoading] = useState(false);
const [newTag, setNewTag] = useState("");
const [albums, setAlbums] = useState<Album[]>([]);
const [albumsError, setAlbumsError] = useState<string | null>(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 (
<main style={{ padding: 16, display: "grid", gap: 20, maxWidth: 720 }}>
<header>
<h1 style={{ marginTop: 0 }}>Admin</h1>
<p style={{ color: "#555" }}>
Manage tags and albums. Admin token is stored in sessionStorage.
</p>
</header>
<section style={{ border: "1px solid #ddd", borderRadius: 12, padding: 16 }}>
<h2 style={{ marginTop: 0 }}>Admin Token</h2>
<form onSubmit={handleSaveToken} style={{ display: "grid", gap: 8 }}>
<input
type="password"
placeholder="X-Porthole-Admin-Token"
value={tokenInput}
onChange={(e) => setTokenInput(e.target.value)}
style={{ padding: 8, borderRadius: 6, border: "1px solid #ccc" }}
/>
<div style={{ display: "flex", gap: 8 }}>
<button type="submit">Save token</button>
<button
type="button"
onClick={() => {
setTokenInput("");
setToken("");
if (typeof window !== "undefined") {
sessionStorage.removeItem(ADMIN_TOKEN_KEY);
}
setTokenMessage("Token cleared.");
}}
>
Clear
</button>
</div>
{tokenMessage ? (
<div style={{ fontSize: 12, color: "#444" }}>{tokenMessage}</div>
) : null}
</form>
</section>
<section style={{ border: "1px solid #ddd", borderRadius: 12, padding: 16 }}>
<h2 style={{ marginTop: 0 }}>Tags</h2>
<form onSubmit={handleCreateTag} style={{ display: "flex", gap: 8 }}>
<input
type="text"
placeholder="New tag name"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }}
/>
<button type="submit">Create</button>
<button type="button" onClick={loadTags} disabled={tagsLoading}>
{tagsLoading ? "Loading..." : "Refresh"}
</button>
</form>
{tagsError ? (
<div style={{ color: "#b00", marginTop: 8 }}>{tagsError}</div>
) : null}
<ul style={{ marginTop: 12, paddingLeft: 16 }}>
{tags.length === 0 ? (
<li style={{ color: "#666" }}>No tags yet.</li>
) : (
tags.map((tag) => <li key={tag.id}>{tag.name}</li>)
)}
</ul>
</section>
<section style={{ border: "1px solid #ddd", borderRadius: 12, padding: 16 }}>
<h2 style={{ marginTop: 0 }}>Albums</h2>
<form onSubmit={handleCreateAlbum} style={{ display: "flex", gap: 8 }}>
<input
type="text"
placeholder="New album name"
value={newAlbum}
onChange={(e) => setNewAlbum(e.target.value)}
style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }}
/>
<button type="submit">Create</button>
<button type="button" onClick={loadAlbums} disabled={albumsLoading}>
{albumsLoading ? "Loading..." : "Refresh"}
</button>
</form>
{albumsError ? (
<div style={{ color: "#b00", marginTop: 8 }}>{albumsError}</div>
) : null}
<ul style={{ marginTop: 12, paddingLeft: 16 }}>
{albums.length === 0 ? (
<li style={{ color: "#666" }}>No albums yet.</li>
) : (
albums.map((album) => <li key={album.id}>{album.name}</li>)
)}
</ul>
</section>
</main>
);
}