import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { createPost, getAdminSession, loginAdmin } from "../api"; import Icon from "../components/Icon"; import PageHeader from "../components/PageHeader"; import { LoadingState } from "../components/Status"; const TOKEN_KEY = "alex-journal-admin"; const today = new Date().toISOString().slice(0, 10); const initialPost = { title: "", excerpt: "", published_at: today, read_time: 5, tags: "AI, Engineering", accent: "mint", section_heading: "The idea", body: "", }; export default function JournalAdminPage() { const navigate = useNavigate(); const [token, setToken] = useState(() => sessionStorage.getItem(TOKEN_KEY) ?? ""); const [checking, setChecking] = useState(Boolean(token)); const [password, setPassword] = useState(""); const [post, setPost] = useState(initialPost); const [status, setStatus] = useState({ type: "idle", message: "" }); useEffect(() => { if (!token) { setChecking(false); return undefined; } const controller = new AbortController(); getAdminSession(token, controller.signal) .catch((error) => { if (error.name !== "AbortError") { sessionStorage.removeItem(TOKEN_KEY); setToken(""); setStatus({ type: "error", message: "Your session ended. Sign in again." }); } }) .finally(() => { if (!controller.signal.aborted) setChecking(false); }); return () => controller.abort(); }, [token]); async function signIn(event) { event.preventDefault(); setStatus({ type: "sending", message: "Signing in…" }); try { const result = await loginAdmin(password); sessionStorage.setItem(TOKEN_KEY, result.access_token); setToken(result.access_token); setPassword(""); setStatus({ type: "success", message: "Signed in. Your session lasts four hours." }); } catch (error) { setStatus({ type: "error", message: error.message }); } } function signOut() { sessionStorage.removeItem(TOKEN_KEY); setToken(""); setStatus({ type: "idle", message: "" }); } function updatePost(event) { setPost((current) => ({ ...current, [event.target.name]: event.target.value })); } async function publish(event) { event.preventDefault(); const paragraphs = post.body .split(/\n\s*\n/) .map((paragraph) => paragraph.trim()) .filter(Boolean); const payload = { title: post.title, excerpt: post.excerpt, published_at: post.published_at, read_time: Number(post.read_time), tags: post.tags.split(",").map((tag) => tag.trim()).filter(Boolean), accent: post.accent, content: [{ heading: post.section_heading, paragraphs }], }; setStatus({ type: "sending", message: "Publishing…" }); try { const created = await createPost(payload, token); setStatus({ type: "success", message: "Published. Opening the article…" }); navigate(`/blog/${created.slug}`); } catch (error) { if (error.status === 401) { sessionStorage.removeItem(TOKEN_KEY); setToken(""); } setStatus({ type: "error", message: error.message }); } } return (
Authenticated : null} /> {checking && } {!checking && !token && (

Private access

Sign in to write

The publisher uses one server-side password. It is never stored in the browser.

{status.message &&

{status.message}

}
)} {!checking && token && (

New article

Paragraphs are separated by a blank line.