"use client"; import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import Markdown from "./Markdown"; interface DocFormProps { mode: "create" | "edit"; slug?: string; secret: string; existingFolders?: Record; existingSections?: string[]; initial?: { title?: string; body?: string; section?: string; type?: string; status?: "published" | "draft" | string; tags?: string[]; author?: string; extraFields?: Record; }; } const DEFAULT_SECTION_LIST = ["docs", "writeups", "research", "notes", "guides", "tutorials", "ctf", "api", "projects"]; const DEFAULT_TYPE_LIST = ["documentation", "writeup", "research", "note", "guide", "tutorial", "spec"]; const PRESET_FIELDS = [ { key: "difficulty", value: "medium" }, { key: "points", value: "100" }, { key: "category", value: "pwn" }, { key: "event", value: "DEF CON Quals 2024" }, { key: "solved", value: "true" }, ]; export default function DocForm({ mode, slug, secret, existingFolders, existingSections, initial, }: DocFormProps) { const router = useRouter(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<"write" | "preview">("write"); 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 ?? ""); const [parentFolder, setParentFolder] = useState(""); const textareaRef = useRef(null); const sectionOptions = Array.from( new Set([...(existingSections ?? []), ...DEFAULT_SECTION_LIST, section].filter(Boolean)), ); // Extract existing custom fields const [customFields, setCustomFields] = useState>(() => { if (initial?.extraFields) { return Object.entries(initial.extraFields) .filter(([k, v]) => k !== "slug" && v !== undefined && v !== null) .map(([k, v]) => ({ key: k, value: typeof v === "string" ? v : JSON.stringify(v) })) .filter((f) => f.value); } return []; }); const tagList = tags .split(",") .map((t) => t.trim()) .filter(Boolean); 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.5 py-2 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[#5e6ad2] transition-colors"; function addCustomField(key = "", value = "") { 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)); } function insertMarkdown(prefix: string, suffix = "") { const textarea = textareaRef.current; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selection = body.substring(start, end); const replacement = `${prefix}${selection || "text"}${suffix}`; const newBody = body.substring(0, start) + replacement + body.substring(end); setBody(newBody); setTimeout(() => { textarea.focus(); textarea.setSelectionRange(start + prefix.length, start + replacement.length - suffix.length); }, 10); } 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; } const payload: Record = { title, body, slug: effectiveSlug, section, type, status, author, tags: tagList, }; 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}
)} {/* Title */}
setTitle(e.target.value)} placeholder="e.g. Database Architecture Overview" className={`${baseInputCls} text-base`} required />
{/* Section + Type + Status */}
setSection(e.target.value)} placeholder="e.g. docs, writeups, ctf, guides" className={baseInputCls} required /> {sectionOptions.map((s) => ( ))}
setType(e.target.value)} placeholder="e.g. documentation, writeup, note" className={baseInputCls} /> {DEFAULT_TYPE_LIST.map((t) => ( ))}
{/* Folder + Slug */}
{isEdit ? ( ) : ( setSlugInput(e.target.value)} className={`${baseInputCls} font-mono`} placeholder="my-new-post" required /> )}

Path: {effectiveSlug || section}

{/* Tags + Author */}
setTags(e.target.value)} className={baseInputCls} placeholder="ctf, pwn, reverse, tutorial" />
setAuthor(e.target.value)} className={baseInputCls} placeholder="asep" />
{/* Custom Metadata Fields */}

Dynamic Custom Metadata

Add custom key-values (event, difficulty, points, etc.) rendered as styled badges.

Presets: {PRESET_FIELDS.map((preset) => ( ))}
{customFields.map((field, i) => (
updateCustomField(i, "key", e.target.value)} className={`${baseInputCls} text-xs font-mono`} /> updateCustomField(i, "value", e.target.value)} className={`${baseInputCls} text-xs font-mono`} />
))}
{/* Body with Write / Preview Tabs */}
{/* Editor Toolbar */}
{/* Tabs */}
{/* Markdown Action Shortcuts */} {activeTab === "write" && (
)}
{/* Tab content */} {activeTab === "write" ? (