feat: Phase 14 — hierarchical folder structure (GitHub-style nested folders)
CI / typecheck + build (turbo) (push) Canceled after 0s
CI / typecheck + build (turbo) (push) Canceled after 0s
- Section index pages ([section]/page.tsx): shows folder tree + flat doc list
- Folder index pages: [section]/[...slug]/page.tsx detects folder paths
and renders subfolder + document listing instead of 404
- Sidebar tree: hierarchical grouping from flat doc slugs, folder icons (📁)
with proper indentation per depth level
- DocForm v2: parent folder dropdown (populated from existing folders),
slug input for leaf name, resolved path display
- Core helpers: extractFoldersForSection + classifyPath exported from @mcpedia/core
- CTF writeup reorganized: pwn-100 ret2win moved into pwn/ subfolder
- _index.md folder intro pages for ctf/ and defcon-quals-2024/
- STANDARD_KEYS includes extraFields to avoid badge duplication
This commit is contained in:
@@ -304,3 +304,64 @@ export async function deleteDocument(slug: string): Promise<{ deleted: boolean }
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 14: Hierarchical folder structure helpers.
|
||||
// These enable GitHub-style nested folder browsing — the content creator
|
||||
// decides folder structure via where they place files; no config needed.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract the folder structure for a given section from a list of document paths.
|
||||
* Returns all distinct folder prefixes (relative to the section), sorted.
|
||||
*
|
||||
* Example: for section "docs" with paths ["docs/a/b/c.md", "docs/a/b/d.md"],
|
||||
* returns ["a", "a/b"].
|
||||
*/
|
||||
export function extractFoldersForSection(
|
||||
docPaths: string[],
|
||||
section: string,
|
||||
): string[] {
|
||||
const folders = new Set<string>();
|
||||
const base = `${section}/`;
|
||||
|
||||
for (const path of docPaths) {
|
||||
const rel = path.startsWith(base) ? path.slice(base.length) : path;
|
||||
const cleanPath = rel.replace(/\.md$/, "");
|
||||
const parts = cleanPath.split("/");
|
||||
|
||||
let acc = "";
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
acc = acc ? `${acc}/${parts[i]}` : parts[i];
|
||||
folders.add(acc);
|
||||
}
|
||||
}
|
||||
|
||||
return [...folders].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a full URL slug path (e.g. "writeups/ctf/defcon-quals-2024"), determine
|
||||
* if it represents a folder (i.e., there are other docs whose paths start with
|
||||
* this prefix) or a leaf document.
|
||||
*
|
||||
* Returns:
|
||||
* - "doc" if the path is a leaf document (path + ".md" matches a doc)
|
||||
* - "folder" if the path is a parent of other doc paths
|
||||
* - "none" if neither
|
||||
*/
|
||||
export function classifyPath(
|
||||
docPaths: string[],
|
||||
path: string,
|
||||
): "doc" | "folder" | "none" {
|
||||
const dotMd = `${path}.md`;
|
||||
|
||||
// Check if it's a leaf document
|
||||
if (docPaths.includes(dotMd)) return "doc";
|
||||
|
||||
// Check if it's a folder (parent of other paths)
|
||||
const prefix = `${path}/`;
|
||||
const hasChildren = docPaths.some((p) => p.startsWith(prefix));
|
||||
if (hasChildren) return "folder";
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user