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 (