fix: remove body field badge, improve sidebar tree styling, use doc titles in homepage tree
CI / typecheck + build (turbo) (push) Canceled after 0s
CI / typecheck + build (turbo) (push) Canceled after 0s
- STANDARD_KEYS now includes 'body' to prevent it from rendering as a custom field badge - Sidebar: tree-style with border-l + ml-2 indentation per level, section separators with border-b, better depth cues - Homepage buildFolderTree: uses doc.title from DB (not path segments), so _index shows 'CTF Writeups' instead of 'Index' - Sidebar: _index path segments no longer show leading-space titles (slugToTitle filters empty)
This commit is contained in:
@@ -325,7 +325,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
|
|||||||
// Any other key in the document metadata becomes a dynamic badge.
|
// Any other key in the document metadata becomes a dynamic badge.
|
||||||
const STANDARD_KEYS = new Set([
|
const STANDARD_KEYS = new Set([
|
||||||
"id", "slug", "title", "type", "section", "status",
|
"id", "slug", "title", "type", "section", "status",
|
||||||
"author", "tags", "path", "createdAt", "updatedAt", "extraFields",
|
"author", "tags", "path", "createdAt", "updatedAt",
|
||||||
|
"extraFields", "body",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Edit mode: inline form
|
// Edit mode: inline form
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] {
|
|||||||
|
|
||||||
for (const doc of docs.filter((d) => d.section === section)) {
|
for (const doc of docs.filter((d) => d.section === section)) {
|
||||||
const parts = doc.slug.split("/");
|
const parts = doc.slug.split("/");
|
||||||
// parts[0] should be the section
|
// parts[0] is the section; skip it
|
||||||
let current = root;
|
let current = root;
|
||||||
|
|
||||||
for (let i = 1; i < parts.length; i++) {
|
for (let i = 1; i < parts.length; i++) {
|
||||||
@@ -56,7 +56,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] {
|
|||||||
} else if (!node.title) {
|
} else if (!node.title) {
|
||||||
node.title = part
|
node.title = part
|
||||||
.split(/[-_]/)
|
.split(/[-_]/)
|
||||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
.map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ""))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
current = node.children;
|
current = node.children;
|
||||||
@@ -91,6 +91,7 @@ function buildFolderTree(docs: Doc[], section: string): TreeNode[] {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively render the folder tree with proper depth styling.
|
* Recursively render the folder tree with proper depth styling.
|
||||||
|
* Each level gets a 20px left margin + subtle border for visual hierarchy.
|
||||||
*/
|
*/
|
||||||
function renderTreeNode(
|
function renderTreeNode(
|
||||||
node: TreeNode,
|
node: TreeNode,
|
||||||
@@ -100,24 +101,24 @@ function renderTreeNode(
|
|||||||
const isFolder = node.children.length > 0;
|
const isFolder = node.children.length > 0;
|
||||||
const isActive = pathname === `/${node.slug}`;
|
const isActive = pathname === `/${node.slug}`;
|
||||||
const hasActiveChild = pathname.startsWith(`/${node.slug}/`);
|
const hasActiveChild = pathname.startsWith(`/${node.slug}/`);
|
||||||
const indent = depth * 16; // 16px per level
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={node.slug}>
|
<li key={node.slug} className={depth > 0 ? "ml-5 border-l border-[#1f2022] pl-2" : ""}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${node.slug}`}
|
href={`/${node.slug}`}
|
||||||
className={`flex items-center gap-1.5 text-xs py-1 px-2 rounded transition-all ${
|
className={`flex items-center justify-between gap-1.5 text-xs py-1 px-2 rounded transition-all ${
|
||||||
isActive
|
isActive
|
||||||
? "text-[#7170ff] bg-[#191a1b]"
|
? "text-[#7170ff] bg-[#191a1b]"
|
||||||
: hasActiveChild
|
: hasActiveChild
|
||||||
? "text-[#d0d6e0]"
|
? "text-[#d0d6e0]"
|
||||||
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#191a1b]"
|
: "text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[#191a1b]"
|
||||||
}`}
|
}`}
|
||||||
style={{ marginLeft: `${indent}px` }}
|
|
||||||
title={node.title}
|
title={node.title}
|
||||||
>
|
>
|
||||||
<span>{isFolder ? "📁" : "📄"}</span>
|
<span className="flex items-center gap-1.5 truncate">
|
||||||
<span className="truncate">{node.title || node.name}</span>
|
<span>{isFolder ? "📁" : "📄"}</span>
|
||||||
|
{node.title || node.name}
|
||||||
|
</span>
|
||||||
{isFolder && node.docCount > 0 && (
|
{isFolder && node.docCount > 0 && (
|
||||||
<span className="ml-auto text-[#62666d] bg-[#191a1b] px-1 py-0 rounded">
|
<span className="ml-auto text-[#62666d] bg-[#191a1b] px-1 py-0 rounded">
|
||||||
{node.docCount}
|
{node.docCount}
|
||||||
@@ -125,7 +126,7 @@ function renderTreeNode(
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
{node.children.length > 0 && (
|
{node.children.length > 0 && (
|
||||||
<ul className="mt-0.5">
|
<ul className="mt-0.5 space-y-0.5">
|
||||||
{node.children.map((child) => renderTreeNode(child, depth + 1, pathname))}
|
{node.children.map((child) => renderTreeNode(child, depth + 1, pathname))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
@@ -156,7 +157,7 @@ export default function Sidebar() {
|
|||||||
{docs.length} documents
|
{docs.length} documents
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2 px-3 text-sm">
|
<ul className="space-y-3 px-3 text-sm">
|
||||||
{SECTIONS.map(({ id, label, icon }) => {
|
{SECTIONS.map(({ id, label, icon }) => {
|
||||||
const sectionDocs = docs.filter((d) => d.section === id);
|
const sectionDocs = docs.filter((d) => d.section === id);
|
||||||
if (sectionDocs.length === 0) return null;
|
if (sectionDocs.length === 0) return null;
|
||||||
@@ -165,16 +166,18 @@ export default function Sidebar() {
|
|||||||
const isActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`);
|
const isActive = pathname === `/${id}` || pathname.startsWith(`/${id}/`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={id}>
|
<li key={id} className="border-b border-[#141516] pb-2 last:border-0">
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-1.5 mb-1 mt-4 first:mt-0 ${
|
className={`flex items-center gap-1.5 mb-1.5 ${
|
||||||
isActive ? "text-[#7170ff]" : "text-[#62666d]"
|
isActive ? "text-[#7170ff]" : "text-[#62666d]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{icon}</span>
|
<span className="text-lg">{icon}</span>
|
||||||
<span className="text-xs font-medium">{label}</span>
|
<span className="font-medium text-xs uppercase tracking-wider">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-0.5 pl-0">
|
<ul className="space-y-0.5">
|
||||||
{tree.map((node) => renderTreeNode(node, 0, pathname))}
|
{tree.map((node) => renderTreeNode(node, 0, pathname))}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
+49
-31
@@ -5,8 +5,8 @@ import { SECTIONS } from "@mcpedia/config/sections";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a hierarchical folder tree from a flat list of document paths.
|
* Build a hierarchical folder tree from document list.
|
||||||
* Each tree node is either a folder (has children) or a leaf doc.
|
* Each node is either a folder (has children) or a leaf doc.
|
||||||
*/
|
*/
|
||||||
interface TreeNode {
|
interface TreeNode {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -16,16 +16,25 @@ interface TreeNode {
|
|||||||
isLeaf: boolean;
|
isLeaf: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFolderTree(paths: string[], section: string): TreeNode[] {
|
// Helper: turn a slug segment into a display title
|
||||||
const tree: TreeNode[] = [];
|
function slugToTitle(segment: string): string {
|
||||||
const base = `${section}/`;
|
return segment
|
||||||
|
.split(/[-_]/)
|
||||||
|
.map((w) => (w ? w.charAt(0).toUpperCase() + w.slice(1) : ""))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
const addPath = (cleanPath: string) => {
|
function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] {
|
||||||
const parts = cleanPath.split("/");
|
const tree: TreeNode[] = [];
|
||||||
|
const treeIndex = new Map<string, TreeNode>();
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
const parts = doc.slug.split("/");
|
||||||
|
// parts[0] should be the section
|
||||||
let current = tree;
|
let current = tree;
|
||||||
let currentPath = section;
|
let currentPath = section;
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 1; i < parts.length; i++) {
|
||||||
const part = parts[i];
|
const part = parts[i];
|
||||||
const isLeaf = i === parts.length - 1;
|
const isLeaf = i === parts.length - 1;
|
||||||
currentPath = `${currentPath}/${part}`;
|
currentPath = `${currentPath}/${part}`;
|
||||||
@@ -35,36 +44,43 @@ function buildFolderTree(paths: string[], section: string): TreeNode[] {
|
|||||||
node = {
|
node = {
|
||||||
name: part,
|
name: part,
|
||||||
slug: currentPath,
|
slug: currentPath,
|
||||||
title: "",
|
title: isLeaf ? doc.title : slugToTitle(part),
|
||||||
children: [],
|
children: [],
|
||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
};
|
};
|
||||||
current.push(node);
|
current.push(node);
|
||||||
|
treeIndex.set(currentPath, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
node.isLeaf = true;
|
node.isLeaf = true;
|
||||||
node.title = part
|
node.title = doc.title;
|
||||||
.split(/[-_]/)
|
|
||||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
||||||
.join(" ");
|
|
||||||
} else if (!node.title) {
|
} else if (!node.title) {
|
||||||
node.title = part
|
node.title = slugToTitle(part);
|
||||||
.split(/[-_]/)
|
|
||||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
||||||
.join(" ");
|
|
||||||
}
|
}
|
||||||
current = node.children;
|
current = node.children;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
for (const docPath of paths) {
|
|
||||||
const rel = docPath.startsWith(base) ? docPath.slice(base.length) : docPath;
|
|
||||||
const cleanPath = rel.replace(/\.md$/, "");
|
|
||||||
addPath(cleanPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tree;
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortAll(nodes: TreeNode[]): TreeNode[] {
|
||||||
|
const sorted = sortNodes(nodes);
|
||||||
|
for (const node of sorted) {
|
||||||
|
if (node.children.length > 0) sortAll(node.children);
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortAll(tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTree(nodes: TreeNode[], pathname: string) {
|
function renderTree(nodes: TreeNode[], pathname: string) {
|
||||||
@@ -82,10 +98,10 @@ function renderTree(nodes: TreeNode[], pathname: string) {
|
|||||||
title={node.title}
|
title={node.title}
|
||||||
>
|
>
|
||||||
<span>{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span>
|
<span>{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span>
|
||||||
{node.title || node.name}
|
<span className="truncate">{node.title || node.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
{node.children.length > 0 && (
|
{node.children.length > 0 && (
|
||||||
<div className="ml-3 mt-0.5">
|
<div className="ml-5 border-l border-[#1f2022] pl-2 mt-0.5">
|
||||||
{renderTree(node.children, pathname)}
|
{renderTree(node.children, pathname)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -97,12 +113,15 @@ function renderTree(nodes: TreeNode[], pathname: string) {
|
|||||||
|
|
||||||
interface SectionTreeProps {
|
interface SectionTreeProps {
|
||||||
section: string;
|
section: string;
|
||||||
docPaths: string[];
|
docs: { slug: string; title: string }[];
|
||||||
pathname: string;
|
pathname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SectionTree({ section, docPaths, pathname }: SectionTreeProps) {
|
function SectionTree({ section, docs, pathname }: SectionTreeProps) {
|
||||||
const tree = buildFolderTree(docPaths.filter((p) => p.startsWith(`${section}/`)), section);
|
const tree = buildFolderTree(
|
||||||
|
docs.filter((d) => d.slug.startsWith(`${section}/`)),
|
||||||
|
section,
|
||||||
|
);
|
||||||
const sectionInfo = SECTIONS.find((s) => s.id === section);
|
const sectionInfo = SECTIONS.find((s) => s.id === section);
|
||||||
if (!sectionInfo) return null;
|
if (!sectionInfo) return null;
|
||||||
|
|
||||||
@@ -123,7 +142,6 @@ function SectionTree({ section, docPaths, pathname }: SectionTreeProps) {
|
|||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const all = await listDocuments();
|
const all = await listDocuments();
|
||||||
const docPaths = all.map((d) => d.path);
|
|
||||||
|
|
||||||
const recent = [...all]
|
const recent = [...all]
|
||||||
.sort(
|
.sort(
|
||||||
@@ -194,7 +212,7 @@ export default async function HomePage() {
|
|||||||
{/* Inline folder tree */}
|
{/* Inline folder tree */}
|
||||||
<SectionTree
|
<SectionTree
|
||||||
section={s.id}
|
section={s.id}
|
||||||
docPaths={docPaths}
|
docs={all.map((d) => ({ slug: d.slug, title: d.title }))}
|
||||||
pathname="/"
|
pathname="/"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user