diff --git a/apps/web/app/[section]/[...slug]/page.tsx b/apps/web/app/[section]/[...slug]/page.tsx index e365199..d51457f 100644 --- a/apps/web/app/[section]/[...slug]/page.tsx +++ b/apps/web/app/[section]/[...slug]/page.tsx @@ -6,11 +6,11 @@ import { WEBHOOK_SECRET } from "@mcpedia/config"; import Markdown from "@/components/Markdown"; import DocForm from "@/components/DocForm"; import TOC from "@/components/TOC"; +import DocActions from "@/components/DocActions"; import { classifyPath, extractFoldersForSection } from "@mcpedia/core"; import { SECTIONS_BY_ID } from "@mcpedia/config/sections"; import type { DocumentMeta } from "@mcpedia/core"; -// Render at request time (content in Postgres, not available at build time). export const dynamic = "force-dynamic"; interface DocPageProps { @@ -18,9 +18,6 @@ interface DocPageProps { searchParams: Promise<{ edit?: string }>; } -/** - * Get section label and icon from the centralized config. - */ function getSectionInfo(section: string) { const info = SECTIONS_BY_ID.get(section); if (!info) { @@ -35,12 +32,12 @@ function getSectionInfo(section: string) { }; } -/** - * Render any extra/custom frontmatter fields as labeled badges. - * This makes the system dynamic โ€” the content creator decides what metadata - * to include. Standard fields (title, author, tags, etc.) are handled - * explicitly; any OTHER keys in frontmatter become badges here. - */ +function calculateReadingTime(text: string): string { + const words = text.trim().split(/\s+/).length; + const minutes = Math.ceil(words / 200); + return `${minutes} min read`; +} + function CustomFieldBadges({ doc, standardKeys, @@ -57,61 +54,49 @@ function CustomFieldBadges({ if (customEntries.length === 0) return null; return ( -
+
{customEntries.map(([key, value]) => { const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "); - let colorClass = "bg-[#191a1b] border-[#23252a] text-[#d0d6e0]"; + let colorClass = "bg-[#141517] border-[#23252a] text-[#d0d6e0]"; + let icon = "๐Ÿท๏ธ"; let displayValue: string; - // --- Auto-detect styling based on VALUE TYPE + VALUE CONTENTS --- - // (NOT key name โ€” content creators determine appearance via values, - // not by using specific key names) - if (typeof value === "number") { colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; - displayValue = `${value}`; + icon = "๐ŸŽฏ"; + displayValue = `${value} pts`; } else if (typeof value === "boolean") { colorClass = value - ? "bg-green-500/15 border-green-500/25 text-green-400" - : "bg-red-500/15 border-red-500/25 text-red-400"; - displayValue = value ? "Yes" : "No"; + ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400" + : "bg-rose-500/10 border-rose-500/30 text-rose-400"; + icon = value ? "โœ“" : "โœ•"; + displayValue = value ? "Solved" : "Pending"; } else if (Array.isArray(value)) { colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; + icon = "๐Ÿ—‚๏ธ"; displayValue = value.join(", "); - } else if (typeof value === "object" && value !== null) { - displayValue = JSON.stringify(value).slice(0, 40) + - (JSON.stringify(value).length > 40 ? "โ€ฆ" : ""); } else { const v = String(value).toLowerCase(); - - const diffMatch = v.match( - /^(easy|simple|beginner)\b|^(medium|intermediate)\b|^(hard|expert|advanced)\b/i, - ); - if (diffMatch) { - if (/^easy|simple|beginner/i.test(v)) - colorClass = "bg-green-500/10 border-green-500/30 text-green-400"; - else if (/^medium|intermediate/i.test(v)) - colorClass = "bg-yellow-500/10 border-yellow-500/30 text-yellow-400"; - else if (/^hard|expert|advanced/i.test(v)) - colorClass = "bg-red-500/10 border-red-500/30 text-red-400"; - displayValue = diffMatch[0]; - } else if (/ctf|def.?con|hack|game|competition|tournament|qualifier/i.test(v)) { + if (/^(easy|simple|beginner)/i.test(v)) { + colorClass = "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"; + icon = "๐ŸŸข"; + displayValue = "Easy"; + } else if (/^(medium|intermediate)/i.test(v)) { + colorClass = "bg-amber-500/10 border-amber-500/30 text-amber-400"; + icon = "๐ŸŸก"; + displayValue = "Medium"; + } else if (/^(hard|expert|advanced)/i.test(v)) { + colorClass = "bg-rose-500/10 border-rose-500/30 text-rose-400"; + icon = "๐Ÿ”ด"; + displayValue = "Hard"; + } else if (/^(pwn|web|crypto|misc|forensic|reverse|binary)/i.test(v)) { + colorClass = "bg-indigo-500/10 border-indigo-500/30 text-indigo-300"; + icon = "โšก"; + displayValue = v.toUpperCase(); + } else if (/ctf|def.?con|event/i.test(v)) { colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; + icon = "๐Ÿ†"; displayValue = String(value); - } else if (/(\d+)\s*pts?$/i.test(v)) { - const match = v.match(/(\d+)\s*pts?$/i); - colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; - displayValue = match ? `${match[1]} pts` : String(value); - } else if (/^(pwn|web|crypto|misc|forensic|reverse|pwnable|binary|webexploit)$/i.test(v)) { - colorClass = "bg-orange-500/15 border-orange-500/25 text-orange-400"; - displayValue = v.charAt(0).toUpperCase() + v.slice(1); - } else if (/^(solved|pending|wip|in.?progress|completed|todo)$/i.test(v)) { - colorClass = v.includes("solved") || v.includes("completed") - ? "bg-green-500/10 border-green-500/30 text-green-400" - : v.includes("wip") || v.includes("progress") - ? "bg-yellow-500/10 border-yellow-500/30 text-yellow-400" - : "bg-red-500/10 border-red-500/30 text-red-400"; - displayValue = v.charAt(0).toUpperCase() + v.slice(1); } else { displayValue = String(value); } @@ -120,10 +105,11 @@ function CustomFieldBadges({ return ( - {displayValue} + {icon} + {label}: + {displayValue} ); })} @@ -131,10 +117,6 @@ function CustomFieldBadges({ ); } -/** - * Folder index page โ€” lists all docs (and subfolders) whose path starts with - * the given prefix. This enables GitHub-style nested folder browsing. - */ function FolderIndexPage({ section, slug, @@ -153,7 +135,6 @@ function FolderIndexPage({ })) .sort((a, b) => a.rel.localeCompare(b.rel)); - // Group into immediate children: folders vs leaf docs const immediateFolders = new Map(); const immediateDocs: DocumentMeta[] = []; @@ -170,129 +151,136 @@ function FolderIndexPage({ } const folders = [...immediateFolders.entries()].sort(([a], [b]) => a.localeCompare(b)); - const sectionInfo = getSectionInfo(section); + const folderName = slug.split("/").pop()!.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); return (
{/* Breadcrumb */} - - {/* History */} - {revisions.length > 0 && ( - - )} - +
+ + {rInfo.icon} {r.section} + + +
+

+ {r.title} +

+ + ); + })} +
+ + )} + + {/* Revision History */} + {revisions.length > 0 && ( + + )} + + + {/* Desktop Sticky Table of Contents Column */} + +
); } diff --git a/apps/web/app/[section]/page.tsx b/apps/web/app/[section]/page.tsx index 00eb55c..19faf49 100644 --- a/apps/web/app/[section]/page.tsx +++ b/apps/web/app/[section]/page.tsx @@ -13,16 +13,12 @@ interface DocMeta { path: string; } -/** - * Build a hierarchical folder tree from a flat list of documents. - * Each node is either a folder (has children) or a leaf doc. - */ interface TreeNode { name: string; slug: string; doc?: DocMeta; children: TreeNode[]; - docCount: number; // total docs in subtree + docCount: number; } function buildFolderTree(docs: DocMeta[], section: string): TreeNode[] { @@ -62,7 +58,6 @@ function buildFolderTree(docs: DocMeta[], section: string): TreeNode[] { } } - // Recursively compute docCount for folder nodes function countDocs(node: TreeNode): number { if (node.doc) return 1; return node.children.reduce((sum, child) => sum + countDocs(child), 0); @@ -77,7 +72,7 @@ function buildFolderTree(docs: DocMeta[], section: string): TreeNode[] { function renderTree(nodes: TreeNode[], pathname: string) { return ( -