/** * CaseLibrary — public-facing case file library. * * The Disclosure Bureau's outward face. Reads /data/ufo/case/reports/*.md, * parses the YAML frontmatter for the case topic, extracts the opening * paragraph as a preview hook, and renders a magazine-style grid. No * mention of "detectives" anywhere — the AI pipeline is plumbing, not the * brand. * * Server component. Single fs.readdir + N small readFile calls (one per * report). Cheap; reports are O(10). */ import Link from "next/link"; import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; interface CaseFile { slug: string; topic: string; topic_pt_br: string | null; /** Opening paragraph of the body, trimmed and language-picked. */ opening: string; mtimeMs: number; } const CASE_ROOT = process.env.CASE_ROOT || "/data/ufo/case"; function parseFrontmatter(md: string): { fm: Record; body: string } { const m = md.match(/^---\n([\s\S]+?)\n---\n([\s\S]*)$/); if (!m) return { fm: {}, body: md }; const fm: Record = {}; for (const line of m[1].split("\n")) { const kv = line.match(/^([a-z_]+):\s*(.+)$/); if (!kv) continue; let v = kv[2].trim(); if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1); fm[kv[1]] = v; } return { fm, body: m[2] }; } /** * Pull the first prose paragraph that looks like a narrative opening. * Skips H1, H2, blockquotes, table separators, and the "(EN)" / "(PT-BR)" * sub-headers. Prefers PT-BR when locale is pt-br. */ function pickOpening(body: string, locale: "pt-br" | "en"): string { const lines = body.split("\n"); const blocks: string[] = []; let cur: string[] = []; for (const raw of lines) { const line = raw.trim(); if (line.length === 0) { if (cur.length > 0) { blocks.push(cur.join(" ")); cur = []; } continue; } if (line.startsWith("#") || line.startsWith(">") || line.startsWith("|") || line.startsWith("---") || line.startsWith("```")) { if (cur.length > 0) { blocks.push(cur.join(" ")); cur = []; } continue; } cur.push(line); } if (cur.length > 0) blocks.push(cur.join(" ")); // For PT-BR locale, prefer a block right after a "(PT-BR)" heading. // For EN locale, prefer the first prose block (the EN sub-section usually // appears first under each act). if (locale === "pt-br") { const ptIdx = body.indexOf("(PT-BR)"); if (ptIdx >= 0) { const after = body.slice(ptIdx); const m = after.match(/\n\n([^\n#>|`-][^\n]+(?:\n[^\n#>|`-][^\n]+)*)/); if (m) return m[1].replace(/\s+/g, " ").trim().slice(0, 400); } } return (blocks[0] ?? "").replace(/\s+/g, " ").trim().slice(0, 400); } async function loadCases(locale: "pt-br" | "en"): Promise { const dir = path.join(CASE_ROOT, "reports"); let files: string[]; try { files = await readdir(dir); } catch { return []; } const items: CaseFile[] = []; for (const f of files) { if (!f.endsWith(".md")) continue; try { const full = path.join(dir, f); const md = await readFile(full, "utf-8"); const st = await stat(full); const { fm, body } = parseFrontmatter(md); items.push({ slug: f.replace(/\.md$/, ""), topic: fm.topic ?? f, topic_pt_br: fm.topic_pt_br ?? null, opening: pickOpening(body, locale), mtimeMs: st.mtimeMs, }); } catch { /* skip broken file */ } } return items.sort((a, b) => b.mtimeMs - a.mtimeMs); } export async function CaseLibrary({ locale, limit, layout = "hero+grid", }: { locale: "pt-br" | "en"; limit?: number; /** "hero+grid" = first case big + rest small (homepage); "grid" = all equal (/bureau) */ layout?: "hero+grid" | "grid"; }) { const allCases = await loadCases(locale); const cases = typeof limit === "number" ? allCases.slice(0, limit) : allCases; if (cases.length === 0) { return (

{locale === "pt-br" ? "Nenhum caso narrado ainda. As primeiras histórias chegam em breve." : "No case files yet. The first stories arrive soon."}

); } const [hero, ...rest] = cases; return (
{locale === "pt-br" ? "// Casos em destaque" : "// Featured case files"}
{layout === "hero+grid" ? ( <> {rest.length > 0 && (
{rest.map((c) => )}
)} ) : (
{cases.map((c) => )}
)}
); } function CaseHero({ c, locale }: { c: CaseFile; locale: "pt-br" | "en" }) { const title = locale === "pt-br" ? (c.topic_pt_br ?? c.topic) : c.topic; return (
{locale === "pt-br" ? "Arquivo desclassificado" : "Declassified case file"}

{title}

{c.opening && (

{c.opening}

)}
{locale === "pt-br" ? "ler o caso completo →" : "read the full case file →"}
); } function CaseCard({ c, locale }: { c: CaseFile; locale: "pt-br" | "en" }) { const title = locale === "pt-br" ? (c.topic_pt_br ?? c.topic) : c.topic; return (

{title}

{c.opening && (

{c.opening}

)}
/c/{c.slug}
); }