// c51 — UI: shells for the admin sxdoc editor. // // Three views: list (GET /admin), edit form (GET /admin/edit/...), and // auth walls for non-admin viewers. Body builders return HTML strings; // the c21 handler wraps them in htmlResponse. // // Fase 2a: raw-HTML textarea editor. Fase 2b adds the block editor on // top — the textarea stays as the underlying form field, and the // block-editor JS will hydrate it into a typed UI. So the form shape // here is forward-compatible with the block editor that lands next. import { escape, renderPage } from "./b51_render_layout.ts"; import type { SxDocumentSummary } from "./a31_sxdoc.ts"; import type { SxDocument } from "./a31_sxdoc.ts"; import { sxToHtml } from "./b51_render_sxdoc.ts"; export const renderAdminList = async (documents: SxDocumentSummary[]): Promise => { const pages = documents.filter((d) => d.type === "page"); const posts = documents.filter((d) => d.type === "post"); const body = `# admin [+ new document](/admin/new) ## pages (${pages.length}) ${pages.length === 0 ? "_no pages yet — migrate or create one._" : adminTable(pages)} ## posts (${posts.length}) ${posts.length === 0 ? "_no posts yet — migrate or create one._" : adminTable(posts)} [← back to home](/) `; return renderPage({ title: "admin — tdd.md", bodyMarkdown: body, noindex: true, }); }; const adminTable = (rows: SxDocumentSummary[]): string => { const lines = rows.map((r) => `| [${escape(r.title)}](/admin/edit/${r.type}/${r.slug}) | \`${escape(r.slug)}\` | ${r.status} | ${r.primaryTag ?? "—"} |`, ); return `| title | slug | status | tag | |---|---|---|---| ${lines.join("\n")}`; }; export interface AdminEditViewModel { mode: "new" | "edit"; title: string; slug: string; type: "page" | "post"; // SxDocument is the canonical input — server projects it to HTML for // the textarea and embeds the JSON for the client editor's hydration. doc: SxDocument; status: "published" | "draft"; primaryTag: string | null; error?: string; } // Embed JSON safely inside " in user content can't break out of the // script tag. JSON.parse handles "<" identically to "<". const safeJsonForScript = (value: unknown): string => JSON.stringify(value).replace(/ => { const action = vm.mode === "new" ? "/admin/new" : `/admin/edit/${vm.type}/${vm.slug}`; const heading = vm.mode === "new" ? "new document" : "edit document"; const submitLabel = vm.mode === "new" ? "Create" : "Save"; const html = sxToHtml(vm.doc); const docJson = safeJsonForScript(vm.doc); const errorBlock = vm.error ? `

${escape(vm.error)}

` : ""; // Delete button uses a separate form to avoid posting the entire edit // payload to the delete endpoint. confirm() catches accidental clicks. const deleteForm = vm.mode === "edit" ? `
` : ""; const form = `
${errorBlock}
Cancel
${deleteForm} `; const title = vm.mode === "new" ? "new — admin — tdd.md" : `${vm.title} — admin — tdd.md`; return renderPage({ title, bodyHtml: `

${heading}

${form}`, noindex: true, }); }; export const renderAdminLoginWall = async (): Promise => renderPage({ title: "admin — sign in — tdd.md", bodyMarkdown: `# admin > Sign in with GitHub to access the admin UI. [ sign in with github → ](/auth/github/start) [← back to home](/)`, noindex: true, }); export const renderAdminNonAdminWall = async (viewer: string): Promise => renderPage({ title: "admin — not authorized — tdd.md", bodyMarkdown: `# not authorized > You are signed in as \`${escape(viewer)}\`, but the admin UI is reserved for the site admin. [← back to home](/) · [your agent](/agents/${escape(viewer)})`, noindex: true, });