"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { SECTIONS } from "@mcpedia/config/sections"; interface Doc { slug: string; title: string; section: string; tags: string[]; } /** * Build a hierarchical folder tree from flat document slugs. * Each node is either a folder (has children) or a leaf doc. */ interface TreeNode { name: string; slug: string; title: string; children: TreeNode[]; isLeaf: boolean; docCount: number; // total docs in subtree } function buildFolderTree(docs: Doc[], section: string): TreeNode[] { const root: TreeNode[] = []; for (const doc of docs.filter((d) => d.section === section)) { const parts = doc.slug.split("/"); // parts[0] is the section; skip it let current = root; for (let i = 1; i < parts.length; i++) { const part = parts[i]; const isLeaf = i === parts.length - 1; let node = current.find((n) => n.name === part); if (!node) { node = { name: part, slug: parts.slice(0, i + 1).join("/"), title: "", children: [], isLeaf: false, docCount: 0, }; current.push(node); } if (isLeaf) { node.isLeaf = true; node.title = doc.title; } else if (!node.title) { node.title = part .split(/[-_]/) .map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : "")) .join(" "); } current = node.children; } } // Recursively compute docCount function countDocs(node: TreeNode): number { if (node.isLeaf) return 1; return node.children.reduce((sum, child) => sum + countDocs(child), 0); } for (const node of root) { node.docCount = countDocs(node); } // Sort: folders first, then docs, alphabetically function sortNodes(nodes: TreeNode[]): TreeNode[] { return nodes.sort((a, b) => { const aFolder = a.children.length > 0 ? 0 : 1; const bFolder = b.children.length > 0 ? 0 : 1; if (aFolder !== bFolder) return aFolder - bFolder; return a.title.localeCompare(b.title); }); } for (const node of root) { sortNodes(node.children); } return sortNodes(root); } /** * Recursively render the folder tree with proper depth styling. * Each level gets a 20px left margin + subtle border for visual hierarchy. */ function renderTreeNode( node: TreeNode, depth: number, pathname: string, ): React.ReactElement { const isFolder = node.children.length > 0; const isActive = pathname === `/${node.slug}`; const hasActiveChild = pathname.startsWith(`/${node.slug}/`); return (
  • 0 ? "ml-5 border-l border-[#1f2022] pl-2" : ""}> {isFolder ? "📁" : "📄"} {node.title || node.name} {isFolder && node.docCount > 0 && ( {node.docCount} )} {node.children.length > 0 && ( )}
  • ); } /** Client-side sidebar (fetches doc list via /api/docs at runtime). * Client component so `next build` SSG doesn't hit Postgres (CI has no DB). */ export default function Sidebar() { const [docs, setDocs] = useState([]); const pathname = usePathname(); useEffect(() => { fetch("/api/docs") .then((r) => r.json()) .then((data) => { if (Array.isArray(data)) setDocs(data); else if (data?.docs) setDocs(data.docs); }) .catch(() => setDocs([])); }, []); return ( ); }