feat: revamp to dynamic database-first architecture and cleanup phase docs
CI / typecheck + build (turbo) (push) Canceled after 0s

- Migrate document fetching and CRUD to be PostgreSQL-authoritative
- Remove static section enums and add dynamic listSections query
- Support custom sections and metadata across API, MCP, and Web UI
- Add /api/sections endpoint and update Header, Sidebar, and Forms
- Remove obsolete phase planning docs and modernize README/AGENTS
This commit is contained in:
asepharyana
2026-08-21 11:35:42 +07:00
parent a937e8f51b
commit a9b67385c3
37 changed files with 720 additions and 2515 deletions
+47 -29
View File
@@ -9,20 +9,21 @@ interface DocFormProps {
slug?: string;
secret: string;
existingFolders?: Record<string, string[]>;
existingSections?: string[];
initial?: {
title?: string;
body?: string;
section?: "docs" | "writeups" | "research" | "notes";
type?: "documentation" | "writeup" | "research" | "note";
status?: "published" | "draft";
section?: string;
type?: string;
status?: "published" | "draft" | string;
tags?: string[];
author?: string;
extraFields?: Record<string, unknown>;
};
}
const SECTION_OPTIONS = ["docs", "writeups", "research", "notes"] as const;
const TYPE_OPTIONS = ["documentation", "writeup", "research", "note"] as const;
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" },
@@ -37,6 +38,7 @@ export default function DocForm({
slug,
secret,
existingFolders,
existingSections,
initial,
}: DocFormProps) {
const router = useRouter();
@@ -56,6 +58,10 @@ export default function DocForm({
const textareaRef = useRef<HTMLTextAreaElement>(null);
const sectionOptions = Array.from(
new Set([...(existingSections ?? []), ...DEFAULT_SECTION_LIST, section].filter(Boolean)),
);
// Extract existing custom fields
const [customFields, setCustomFields] = useState<Array<{ key: string; value: string }>>(() => {
if (initial?.extraFields) {
@@ -198,7 +204,7 @@ export default function DocForm({
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g. DEF CON Quals 2024 — pwn-100 Writeup"
placeholder="e.g. Database Architecture Overview"
className={`${baseInputCls} text-base`}
required
/>
@@ -210,34 +216,46 @@ export default function DocForm({
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Section
</label>
<select
value={section}
onChange={(e) => setSection(e.target.value as typeof section)}
className={baseInputCls}
disabled={isEdit}
>
{SECTION_OPTIONS.map((s) => (
<option key={s} value={s}>
{s.toUpperCase()}
</option>
))}
</select>
<div className="relative">
<input
type="text"
list="section-list"
value={section}
onChange={(e) => setSection(e.target.value)}
placeholder="e.g. docs, writeups, ctf, guides"
className={baseInputCls}
required
/>
<datalist id="section-list">
{sectionOptions.map((s) => (
<option key={s} value={s}>
{s.toUpperCase()}
</option>
))}
</datalist>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
Type
</label>
<select
value={type}
onChange={(e) => setType(e.target.value as typeof type)}
className={baseInputCls}
>
{TYPE_OPTIONS.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<div className="relative">
<input
type="text"
list="type-list"
value={type}
onChange={(e) => setType(e.target.value)}
placeholder="e.g. documentation, writeup, note"
className={baseInputCls}
/>
<datalist id="type-list">
{DEFAULT_TYPE_LIST.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</datalist>
</div>
</div>
<div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5">
+16 -4
View File
@@ -1,15 +1,27 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { SECTIONS } from "@mcpedia/config/sections";
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
import ThemeToggle from "@/components/ThemeToggle";
import CommandMenu from "@/components/CommandMenu";
export default function Header() {
const pathname = usePathname();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [sections, setSections] = useState<SectionConfig[]>(DEFAULT_SECTIONS);
useEffect(() => {
fetch("/api/sections")
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data) && data.length > 0) {
setSections(data);
}
})
.catch(() => {});
}, []);
return (
<header className="sticky top-0 z-30 border-b border-[var(--border-color)] glass-nav">
@@ -31,7 +43,7 @@ export default function Header() {
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-1">
{SECTIONS.map((s) => {
{sections.map((s) => {
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
return (
<Link
@@ -107,7 +119,7 @@ export default function Header() {
{/* Mobile Drawer */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-[var(--border-color)] bg-[var(--bg-surface)] px-4 py-3 space-y-1 animate-fade-in shadow-lg">
{SECTIONS.map((s) => (
{sections.map((s) => (
<Link
key={s.id}
href={`/${s.id}`}
+15 -4
View File
@@ -3,7 +3,7 @@
import { useEffect, useState, useMemo } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { SECTIONS } from "@mcpedia/config/sections";
import { getSectionMeta } from "@mcpedia/config/sections";
interface Doc {
slug: string;
@@ -200,6 +200,17 @@ export default function Sidebar() {
);
}, [docs, filterText]);
const distinctSections = useMemo(() => {
const map = new Map<string, { id: string; label: string; icon: string }>();
for (const doc of filteredDocs) {
const s = (doc.section || "docs").toLowerCase();
if (!map.has(s)) {
map.set(s, getSectionMeta(s));
}
}
return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label));
}, [filteredDocs]);
return (
<nav className="h-full flex flex-col py-4 px-3">
{/* Sidebar search filter */}
@@ -233,14 +244,14 @@ export default function Sidebar() {
{/* Summary count */}
<div className="flex items-center justify-between px-1 mb-3 text-[11px] text-[var(--text-dim)]">
<span className="uppercase tracking-wider font-mono">Documentation</span>
<span className="uppercase tracking-wider font-mono">Knowledge Base</span>
<span>{filteredDocs.length} items</span>
</div>
{/* Section Trees */}
<div className="flex-1 overflow-y-auto space-y-4 pr-1">
{SECTIONS.map(({ id, label, icon }) => {
const sectionDocs = filteredDocs.filter((d) => d.section === id);
{distinctSections.map(({ id, label, icon }) => {
const sectionDocs = filteredDocs.filter((d) => (d.section || "docs").toLowerCase() === id);
if (sectionDocs.length === 0) return null;
const tree = buildFolderTree(filteredDocs, id);