"use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; interface DocFormProps { mode: "create" | "edit"; slug?: string; secret: string; existingFolders?: Record; initial?: { title?: string; body?: string; section?: "docs" | "writeups" | "research" | "notes"; type?: "documentation" | "writeup" | "research" | "note"; status?: "published" | "draft"; tags?: string[]; author?: string; extraFields?: Record; }; } const SECTION_OPTIONS = ["docs", "writeups", "research", "notes"] as const; const TYPE_OPTIONS = ["documentation", "writeup", "research", "note"] as const; export default function DocForm({ mode, slug, secret, existingFolders, initial, }: DocFormProps) { const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [title, setTitle] = useState(initial?.title ?? ""); const [slugInput, setSlugInput] = useState(""); const [body, setBody] = useState(initial?.body ?? ""); const [section, setSection] = useState(initial?.section ?? "docs"); const [type, setType] = useState(initial?.type ?? "documentation"); const [status, setStatus] = useState(initial?.status ?? "published"); const [tags, setTags] = useState(initial?.tags?.join(", ") ?? ""); const [author, setAuthor] = useState(initial?.author ?? ""); // Dynamic custom fields — content creators can add any metadata they want const [customFields, setCustomFields] = useState< Array<{ key: string; value: string }> >([]); // Parent folder selection (hierarchical structure like GitHub folders) const [parentFolder, setParentFolder] = useState(""); const tagList = tags .split(",") .map((t) => t.trim()) .filter(Boolean); // For edit mode, slug is fixed (full path). For create mode, build from folder + slugInput. const isEdit = mode === "edit"; const effectiveSlug = isEdit ? (slug ?? "") : parentFolder ? `${parentFolder}/${slugInput}`.replace(/^\/+/, "").replace(/\/+/g, "/") : slugInput; const availableFolders = existingFolders?.[section] ?? []; const baseInputCls = "w-full px-3 py-2 bg-[#0f1011] border border-[#23252a] rounded text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2]"; function addCustomField() { setCustomFields([...customFields, { key: "", value: "" }]); } function updateCustomField(index: number, field: "key" | "value", value: string) { const updated = [...customFields]; updated[index][field] = value; setCustomFields(updated); } function removeCustomField(index: number) { setCustomFields(customFields.filter((_, i) => i !== index)); } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setLoading(true); setError(null); if (!effectiveSlug || !title || !body) { setError("Slug, title, and body are required."); setLoading(false); return; } // Build payload with custom fields flattened at top level const payload: Record = { title, body, slug: effectiveSlug, section, type, status, author, tags: tagList, }; // Merge dynamic custom fields for (const field of customFields) { if (field.key.trim() && field.value.trim()) { payload[field.key.trim()] = field.value.trim(); } } try { let res: Response; if (mode === "create") { res = await fetch("/api/docs", { method: "POST", headers: { "Content-Type": "application/json", "x-webhook-secret": secret, }, body: JSON.stringify(payload), }); } else { const editSlug = slug ?? ""; res = await fetch(`/api/docs/${editSlug}`, { method: "PUT", headers: { "Content-Type": "application/json", "x-webhook-secret": secret, }, body: JSON.stringify(payload), }); } if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error ?? "Save failed"); } const result = await res.json(); router.push(result.doc?.slug ? `/${result.doc.slug}` : "/"); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } } return (
{error && (
{error}
)}
setTitle(e.target.value)} className={baseInputCls} required />

Place this document inside an existing folder. The slug will be prepended with the folder path (e.g. selecting "ctf/defcon-quals-2024" + slug "pwn-100" → "ctf/defcon-quals-2024/pwn-100").

{isEdit ? ( ) : ( setSlugInput(e.target.value)} className={baseInputCls} placeholder="my-document-slug" required /> )}

Resolved path: {effectiveSlug || section}

setTags(e.target.value)} className={baseInputCls} placeholder="comma, separated, tags" />
setAuthor(e.target.value)} className={baseInputCls} />
{/* Dynamic custom fields — creators add whatever metadata they need */}

Add extra frontmatter fields. Content creators name and define their own metadata here — the system auto-renders any field as a badge on the doc page based on its value.

{customFields.length > 0 && (
{customFields.map((field, i) => (
updateCustomField(i, "key", e.target.value)} className="flex-1 px-3 py-2 bg-[#0f1011] border border-[#23252a] rounded text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2]" /> updateCustomField(i, "value", e.target.value)} className="flex-1 px-3 py-2 bg-[#0f1011] border border-[#23252a] rounded text-[#f7f8f8] placeholder-[#62666d] focus:outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2]" />
))}
)}