"use client"; import { useEffect, useState, useMemo } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { getSectionMeta } from "@mcpedia/config/sections"; interface Doc { slug: string; title: string; section: string; tags: string[]; } interface TreeNode { name: string; slug: string; title: string; children: TreeNode[]; isLeaf: boolean; docCount: number; } 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("/"); 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; } } 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); } 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 || a.name).localeCompare(b.title || b.name); }); } for (const node of root) { sortNodes(node.children); } return sortNodes(root); } function TreeNodeItem({ node, depth, pathname, collapsedMap, toggleCollapse, }: { node: TreeNode; depth: number; pathname: string; collapsedMap: Record; toggleCollapse: (slug: string) => void; }) { const isFolder = node.children.length > 0; const isActive = pathname === `/${node.slug}`; const isParentOfActive = pathname.startsWith(`/${node.slug}/`); const isCollapsed = collapsedMap[node.slug] ?? false; return (
  • 0 ? "ml-3 pl-2 border-l border-[var(--border-color)]" : ""}`}>
    {isFolder ? "📁" : "📄"} {node.title || node.name} {isFolder && (
    {node.docCount}
    )}
    {isFolder && !isCollapsed && ( )}
  • ); } export default function Sidebar() { const [docs, setDocs] = useState([]); const [filterText, setFilterText] = useState(""); const [collapsedMap, setCollapsedMap] = 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([])); }, []); function toggleCollapse(slug: string) { setCollapsedMap((prev) => ({ ...prev, [slug]: !prev[slug] })); } const filteredDocs = useMemo(() => { if (!filterText.trim()) return docs; const lower = filterText.toLowerCase(); return docs.filter( (d) => d.title.toLowerCase().includes(lower) || d.slug.toLowerCase().includes(lower) || d.tags.some((t) => t.toLowerCase().includes(lower)), ); }, [docs, filterText]); const distinctSections = useMemo(() => { const map = new Map(); 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 ( ); }