feat(web): overhaul frontend with cyber-intelligence design and interactive topology
CI / typecheck + tests (push) Canceled after 0s
CI / build + deploy (Nix) — api (push) Canceled after 0s
CI / build + deploy (Nix) — mcp (push) Canceled after 0s
CI / build + deploy (Nix) — worker (push) Canceled after 0s
CI / build + deploy (web) (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-08-22 14:55:19 +07:00
parent 9591c67478
commit 011571e12d
22 changed files with 1763 additions and 869 deletions
+125 -88
View File
@@ -11,6 +11,24 @@ import PdfExportView from "@/components/PdfExportView";
import { classifyPath, extractFoldersForSection } from "@mcpedia/core"; import { classifyPath, extractFoldersForSection } from "@mcpedia/core";
import { getSectionMeta } from "@mcpedia/config"; import { getSectionMeta } from "@mcpedia/config";
import type { DocumentMeta } from "@mcpedia/core"; import type { DocumentMeta } from "@mcpedia/core";
import {
Folder,
FileText,
Clock,
User,
Tag,
History,
FileDown,
ArrowLeft,
BookOpen,
Sparkles,
ShieldCheck,
CheckCircle2,
XCircle,
Target,
Trophy,
Zap,
} from "lucide-react";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -45,48 +63,45 @@ function CustomFieldBadges({
if (customEntries.length === 0) return null; if (customEntries.length === 0) return null;
return ( return (
<div className="flex flex-wrap items-center gap-2 mt-4 pt-3.5 border-t border-[var(--border-color)]"> <div className="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-[var(--border-color)]">
{customEntries.map(([key, value]) => { {customEntries.map(([key, value]) => {
const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "); const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " ");
let colorClass = "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-secondary)]"; let colorClass = "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-secondary)]";
let icon = "🏷️";
let displayValue: string; let displayValue: string;
let IconComponent = Tag;
if (typeof value === "number") { if (typeof value === "number") {
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]"; colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
icon = "🎯"; IconComponent = Target;
displayValue = `${value} pts`; displayValue = `${value} pts`;
} else if (typeof value === "boolean") { } else if (typeof value === "boolean") {
colorClass = value colorClass = value
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400" ? "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400"
: "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400"; : "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400";
icon = value ? "✓" : "✕"; IconComponent = value ? CheckCircle2 : XCircle;
displayValue = value ? "Solved" : "Pending"; displayValue = value ? "Solved" : "Pending";
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]"; colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
icon = "🗂️"; IconComponent = Folder;
displayValue = value.join(", "); displayValue = value.join(", ");
} else { } else {
const v = String(value).toLowerCase(); const v = String(value).toLowerCase();
if (/^(easy|simple|beginner)/i.test(v)) { if (/^(easy|simple|beginner)/i.test(v)) {
colorClass = "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400"; colorClass = "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400";
icon = "🟢";
displayValue = "Easy"; displayValue = "Easy";
} else if (/^(medium|intermediate)/i.test(v)) { } else if (/^(medium|intermediate)/i.test(v)) {
colorClass = "bg-amber-500/10 border-amber-500/30 text-amber-600 dark:text-amber-400"; colorClass = "bg-amber-500/10 border-amber-500/30 text-amber-600 dark:text-amber-400";
icon = "🟡";
displayValue = "Medium"; displayValue = "Medium";
} else if (/^(hard|expert|advanced)/i.test(v)) { } else if (/^(hard|expert|advanced)/i.test(v)) {
colorClass = "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400"; colorClass = "bg-rose-500/10 border-rose-500/30 text-rose-600 dark:text-rose-400";
icon = "🔴";
displayValue = "Hard"; displayValue = "Hard";
} else if (/^(pwn|web|crypto|misc|forensic|reverse|binary)/i.test(v)) { } else if (/^(pwn|web|crypto|misc|forensic|reverse|binary)/i.test(v)) {
colorClass = "bg-indigo-500/10 border-indigo-500/30 text-indigo-600 dark:text-indigo-300"; colorClass = "bg-indigo-500/10 border-indigo-500/30 text-indigo-600 dark:text-indigo-300";
icon = "⚡"; IconComponent = Zap;
displayValue = v.toUpperCase(); displayValue = v.toUpperCase();
} else if (/ctf|def.?con|event/i.test(v)) { } else if (/ctf|def.?con|event/i.test(v)) {
colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]"; colorClass = "bg-[var(--brand)]/10 border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)]";
icon = "🏆"; IconComponent = Trophy;
displayValue = String(value); displayValue = String(value);
} else { } else {
displayValue = String(value); displayValue = String(value);
@@ -96,11 +111,11 @@ function CustomFieldBadges({
return ( return (
<span <span
key={key} key={key}
className={`inline-flex items-center gap-1.5 px-2.5 py-1 border rounded-md text-xs font-mono transition-colors ${colorClass}`} className={`inline-flex items-center gap-1.5 px-3 py-1 border rounded-lg text-xs font-mono transition-colors ${colorClass}`}
> >
<span className="text-[10px] opacity-70">{icon}</span> <IconComponent className="w-3.5 h-3.5 opacity-80" />
<span className="text-[var(--text-muted)] font-sans">{label}:</span> <span className="text-[var(--text-muted)] font-sans">{label}:</span>
<span className="font-medium">{displayValue}</span> <span className="font-semibold">{displayValue}</span>
</span> </span>
); );
})} })}
@@ -146,9 +161,9 @@ function FolderIndexPage({
const folderName = slug.split("/").pop()!.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "); const folderName = slug.split("/").pop()!.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
return ( return (
<div> <div className="space-y-8">
{/* Breadcrumb */} {/* Breadcrumb Navigation */}
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] mb-6 flex-wrap"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] flex-wrap">
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
MCPedia MCPedia
</Link> </Link>
@@ -171,58 +186,61 @@ function FolderIndexPage({
})} })}
</nav> </nav>
{/* Header card */} {/* Header Card */}
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 mb-8 shadow-sm"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-7 shadow-sm">
<div className="flex items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3.5">
<div className="w-10 h-10 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-xl"> <div className="w-12 h-12 rounded-2xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-[var(--brand)] dark:text-[var(--accent)] shadow-inner">
📁 <Folder className="w-6 h-6" />
</div> </div>
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight"> <h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
{folderName} {folderName}
</h1> </h1>
<p className="text-xs text-[var(--text-muted)] mt-0.5"> <p className="text-xs text-[var(--text-muted)] mt-1 font-mono">
Directory path: <code className="text-[var(--brand)] dark:text-[var(--accent)] font-mono">{section}/{slug}</code> Directory: <code className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{section}/{slug}</code>
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2.5 py-1 rounded-md"> <span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-3 py-1.5 rounded-lg">
{immediateDocs.length + folders.length} item{folders.length + immediateDocs.length !== 1 ? "s" : ""} {immediateDocs.length + folders.length} item{folders.length + immediateDocs.length !== 1 ? "s" : ""}
</span> </span>
<Link <Link
href={`/${section}/${slug}/export`} href={`/${section}/${slug}/export`}
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3 py-1.5 rounded-lg font-medium transition-all shadow-xs" className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3.5 py-1.5 rounded-lg font-semibold transition-all shadow-md shadow-[var(--brand)]/20"
title={`Export all ${folderName} documents as a single PDF report`} title={`Export all ${folderName} documents as a single PDF report`}
> >
<span>📄</span> <FileDown className="w-3.5 h-3.5" />
<span>Export PDF Report</span> <span>Export PDF Report</span>
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
{/* Subfolders */} {/* Subfolders Grid */}
{folders.length > 0 && ( {folders.length > 0 && (
<div className="mb-8"> <div>
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 tracking-wider flex items-center gap-2"> <h2 className="text-xs font-bold text-[var(--text-muted)] uppercase mb-3.5 tracking-wider font-mono flex items-center gap-2">
<span>Subfolders</span> <Folder className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
<span className="text-[10px] text-[var(--text-dim)]">({folders.length})</span> <span>Subfolders ({folders.length})</span>
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{folders.map(([folder, info]) => ( {folders.map(([folder, info]) => (
<Link <Link
key={folder} key={folder}
href={`/${section}/${slug}/${folder}`.replace(/\/+/g, "/")} href={`/${section}/${slug}/${folder}`.replace(/\/+/g, "/")}
className="group flex items-center gap-3 p-3.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg transition-all shadow-sm" className="group flex items-center gap-3.5 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<span className="text-2xl group-hover:scale-110 transition-transform">📁</span> <div className="p-2 rounded-xl bg-[var(--bg-elevated)] text-[var(--brand)] dark:text-[var(--accent)]">
<Folder className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate"> <div className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
{folder.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")} {folder.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
</div> </div>
<div className="text-xs text-[var(--text-muted)] mt-0.5 font-mono"> <div className="text-xs text-[var(--text-muted)] font-mono mt-0.5">
{info.docCount} document{info.docCount !== 1 ? "s" : ""} {info.docCount} document{info.docCount !== 1 ? "s" : ""}
</div> </div>
</div> </div>
@@ -232,32 +250,34 @@ function FolderIndexPage({
</div> </div>
)} )}
{/* Documents */} {/* Documents List */}
{immediateDocs.length > 0 && ( {immediateDocs.length > 0 && (
<div> <div>
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 tracking-wider flex items-center gap-2"> <h2 className="text-xs font-bold text-[var(--text-muted)] uppercase mb-3.5 tracking-wider font-mono flex items-center gap-2">
<span>Documents</span> <FileText className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
<span className="text-[10px] text-[var(--text-dim)]">({immediateDocs.length})</span> <span>Documents ({immediateDocs.length})</span>
</h2> </h2>
<div className="space-y-2"> <div className="space-y-2.5">
{immediateDocs.map((doc) => ( {immediateDocs.map((doc) => (
<Link <Link
key={doc.slug} key={doc.slug}
href={`/${doc.slug}`} href={`/${doc.slug}`}
className="group flex items-center justify-between gap-4 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg transition-all shadow-sm" className="group flex items-center justify-between gap-4 p-4 sm:p-5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3.5 min-w-0">
<span className="text-xl opacity-70 group-hover:opacity-100">📄</span> <div className="p-2 rounded-xl bg-[var(--bg-elevated)] text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
<FileText className="w-4 h-4" />
</div>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate"> <div className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
{doc.title} {doc.title}
</div> </div>
{doc.tags.length > 0 && ( {doc.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-1.5"> <div className="flex flex-wrap gap-1.5 mt-2">
{doc.tags.slice(0, 4).map((t) => ( {doc.tags.slice(0, 4).map((t) => (
<span <span
key={t} key={t}
className="text-[11px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]" className="text-[10px] px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] font-mono"
> >
#{t} #{t}
</span> </span>
@@ -266,11 +286,15 @@ function FolderIndexPage({
)} )}
</div> </div>
</div> </div>
<time className="text-xs text-[var(--text-dim)] shrink-0 font-mono">
<time className="text-xs text-[var(--text-dim)] shrink-0 font-mono flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>
{new Date(doc.updatedAt).toLocaleDateString(undefined, { {new Date(doc.updatedAt).toLocaleDateString(undefined, {
month: "short", month: "short",
day: "numeric", day: "numeric",
})} })}
</span>
</time> </time>
</Link> </Link>
))} ))}
@@ -279,7 +303,7 @@ function FolderIndexPage({
)} )}
{folders.length === 0 && immediateDocs.length === 0 && ( {folders.length === 0 && immediateDocs.length === 0 && (
<div className="text-center py-12 border border-[var(--border-color)] rounded-xl bg-[var(--bg-surface)]"> <div className="text-center py-16 border border-[var(--border-color)] rounded-2xl bg-[var(--bg-surface)]">
<p className="text-sm text-[var(--text-muted)]">No documents found in this directory.</p> <p className="text-sm text-[var(--text-muted)]">No documents found in this directory.</p>
</div> </div>
)} )}
@@ -331,14 +355,15 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
if (edit === "1" && canEdit) { if (edit === "1" && canEdit) {
const existingFolders = extractFoldersForSection(docSlugs, doc.section); const existingFolders = extractFoldersForSection(docSlugs, doc.section);
return ( return (
<div> <div className="space-y-6">
<Link <Link
href={`/${doc.slug}`} href={`/${doc.slug}`}
className="inline-flex items-center gap-1 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] mb-4 transition-colors" className="inline-flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
> >
<span>← Back to document</span> <ArrowLeft className="w-3.5 h-3.5" />
<span>Back to document</span>
</Link> </Link>
<h1 className="text-2xl font-bold text-[var(--text-primary)] mb-6"> <h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
Edit: {doc.title} Edit: {doc.title}
</h1> </h1>
<DocForm <DocForm
@@ -373,7 +398,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
<div className="flex items-start gap-8"> <div className="flex items-start gap-8">
{/* Main Content Column */} {/* Main Content Column */}
<article className="flex-1 min-w-0"> <article className="flex-1 min-w-0">
{/* Breadcrumb */} {/* Breadcrumbs Navigation */}
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] mb-4 flex-wrap"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)] mb-4 flex-wrap">
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors">
MCPedia MCPedia
@@ -399,43 +424,47 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</nav> </nav>
{/* Hero Metadata Card */} {/* Hero Metadata Card */}
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 mb-8 shadow-sm"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-7 mb-8 shadow-sm">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-4"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-5 mb-5">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2 flex-wrap"> <div className="flex items-center gap-2 mb-2.5 flex-wrap">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono font-medium bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30"> <span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-mono font-bold bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30 uppercase">
<span>{sectionInfo.icon}</span> <span>{sectionInfo.icon}</span>
<span className="uppercase">{sectionInfo.label}</span> <span>{sectionInfo.label}</span>
</span> </span>
<span className="text-xs text-[var(--text-dim)]">·</span> <span className="text-xs text-[var(--text-dim)]">·</span>
<span className="text-xs text-[var(--text-muted)]"> <span className="text-xs text-[var(--text-muted)] font-medium">
{doc.type.charAt(0).toUpperCase() + doc.type.slice(1)} {doc.type.charAt(0).toUpperCase() + doc.type.slice(1)}
</span> </span>
<span className="text-xs text-[var(--text-dim)]">·</span> <span className="text-xs text-[var(--text-dim)]">·</span>
<span className="text-xs text-[var(--text-muted)] font-mono"> <span className="text-xs text-[var(--text-muted)] font-mono flex items-center gap-1">
{readingTime} <Clock className="w-3 h-3" />
<span>{readingTime}</span>
</span> </span>
</div> </div>
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight leading-tight">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-extrabold text-[var(--text-primary)] tracking-tight leading-tight">
{doc.title} {doc.title}
</h1> </h1>
</div> </div>
{/* Quick Actions */} {/* Quick Actions Deck */}
<DocActions slug={doc.slug} body={doc.body} canEdit={canEdit} /> <DocActions slug={doc.slug} body={doc.body} canEdit={canEdit} />
</div> </div>
{/* Author + Date + Tags */} {/* Author, Date, and Tags */}
<div className="flex flex-wrap items-center gap-3 text-xs text-[var(--text-muted)] pt-3 border-t border-[var(--border-color)]"> <div className="flex flex-wrap items-center gap-3 text-xs text-[var(--text-muted)] pt-4 border-t border-[var(--border-color)]">
{doc.author && ( {doc.author && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-4 h-4 rounded-full bg-[var(--brand)] text-white flex items-center justify-center text-[10px] font-bold"> <div className="w-4 h-4 rounded-full bg-[var(--brand)] text-white flex items-center justify-center text-[10px] font-bold">
{doc.author.charAt(0).toUpperCase()} {doc.author.charAt(0).toUpperCase()}
</div> </div>
<span className="text-[var(--text-secondary)]">{doc.author}</span> <span className="text-[var(--text-secondary)] font-medium">{doc.author}</span>
<span className="text-[var(--text-dim)]">·</span> <span className="text-[var(--text-dim)]">·</span>
</div> </div>
)} )}
<span className="flex items-center gap-1">
<Clock className="w-3 h-3 text-[var(--text-dim)]" />
<span> <span>
Updated {new Date(doc.updatedAt).toLocaleDateString(undefined, { Updated {new Date(doc.updatedAt).toLocaleDateString(undefined, {
month: "long", month: "long",
@@ -443,6 +472,8 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
year: "numeric", year: "numeric",
})} })}
</span> </span>
</span>
{doc.tags.length > 0 && ( {doc.tags.length > 0 && (
<> <>
<span className="text-[var(--text-dim)]">·</span> <span className="text-[var(--text-dim)]">·</span>
@@ -450,7 +481,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
{doc.tags.map((t) => ( {doc.tags.map((t) => (
<span <span
key={t} key={t}
className="px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)] text-[11px]" className="px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] text-[11px] font-mono"
> >
#{t} #{t}
</span> </span>
@@ -465,32 +496,35 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</div> </div>
{/* Mobile TOC (rendered inline on smaller screens) */} {/* Mobile TOC (rendered inline on smaller screens) */}
<div className="xl:hidden mb-6 p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg shadow-sm"> <div className="xl:hidden mb-6 p-5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-xs">
<TOC source={doc.body} /> <TOC source={doc.body} />
</div> </div>
{/* Markdown content */} {/* Markdown Content */}
<div className="mb-14"> <div className="mb-14">
<Markdown source={doc.body} /> <Markdown source={doc.body} />
</div> </div>
{/* Related Documents */} {/* Related Documents Grid */}
{related.length > 0 && ( {related.length > 0 && (
<aside className="mt-12 pt-8 border-t border-[var(--border-color)]"> <aside className="mt-14 pt-8 border-t border-[var(--border-color)]">
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4 flex items-center gap-2"> <div className="flex items-center gap-2 mb-4">
<span>Related Knowledge</span> <Sparkles className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
Related Knowledge & Writeups
</h2> </h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
{related.map((r) => { {related.map((r) => {
const rInfo = getSectionInfo(r.section); const rInfo = getSectionInfo(r.section);
return ( return (
<Link <Link
key={r.slug} key={r.slug}
href={`/${r.slug}`} href={`/${r.slug}`}
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg p-4 transition-all shadow-sm" className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<div className="flex items-center justify-between gap-2 mb-2"> <div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs text-[var(--brand)] dark:text-[var(--accent)] font-mono uppercase"> <span className="text-xs text-[var(--brand)] dark:text-[var(--accent)] font-mono uppercase font-semibold">
{rInfo.icon} {r.section} {rInfo.icon} {r.section}
</span> </span>
<time className="text-[11px] text-[var(--text-dim)] font-mono"> <time className="text-[11px] text-[var(--text-dim)] font-mono">
@@ -500,7 +534,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
})} })}
</time> </time>
</div> </div>
<h3 className="text-sm font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-1"> <h3 className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-1">
{r.title} {r.title}
</h3> </h3>
</Link> </Link>
@@ -510,28 +544,31 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
</aside> </aside>
)} )}
{/* Revision History */} {/* Revision History Timeline */}
{revisions.length > 0 && ( {revisions.length > 0 && (
<aside className="mt-10 pt-8 border-t border-[var(--border-color)]"> <aside className="mt-10 pt-8 border-t border-[var(--border-color)]">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider"> <div className="flex items-center gap-2">
<History className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
Revision History ({revisions.length}) Revision History ({revisions.length})
</h2> </h2>
</div> </div>
</div>
<div className="space-y-2"> <div className="space-y-2">
{revisions.map((rev) => ( {revisions.map((rev) => (
<div <div
key={rev.id} key={rev.id}
className="flex items-center justify-between gap-3 p-3 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-xs shadow-sm" className="flex items-center justify-between gap-3 p-3.5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl text-xs shadow-xs"
> >
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2.5 min-w-0">
<span className="font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 px-1.5 py-0.5 rounded border border-[var(--brand)]/30 font-semibold"> <span className="font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 px-2 py-0.5 rounded-md border border-[var(--brand)]/30 font-bold">
v{rev.revisionNo} v{rev.revisionNo}
</span> </span>
<span className="text-[var(--text-primary)] truncate"> <span className="text-[var(--text-primary)] font-medium truncate">
{rev.reason || "Updated document"} {rev.reason || "Updated document in PostgreSQL"}
</span> </span>
<span className="text-[var(--text-dim)] hidden sm:inline"> <span className="text-[var(--text-dim)] hidden sm:inline font-mono">
· {new Date(rev.createdAt).toLocaleString()} · {new Date(rev.createdAt).toLocaleString()}
</span> </span>
</div> </div>
@@ -540,7 +577,7 @@ export default async function DocPage({ params, searchParams }: DocPageProps) {
<input type="hidden" name="id" value={rev.id} /> <input type="hidden" name="id" value={rev.id} />
<button <button
type="submit" type="submit"
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-2.5 py-1 rounded border border-[var(--border-color)] hover:border-[var(--brand)] transition-colors font-medium shadow-sm" className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] px-3 py-1 rounded-lg border border-[var(--border-color)] hover:border-[var(--brand)] transition-colors font-semibold shadow-xs cursor-pointer"
> >
Restore Restore
</button> </button>
+70 -39
View File
@@ -1,6 +1,17 @@
import Link from "next/link"; import Link from "next/link";
import { listDocuments } from "@mcpedia/core"; import { listDocuments } from "@mcpedia/core";
import { getSectionMeta } from "@mcpedia/config"; import { getSectionMeta } from "@mcpedia/config";
import {
Folder,
FileText,
ChevronRight,
FileDown,
Plus,
Compass,
ArrowLeft,
Clock,
Tag,
} from "lucide-react";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -81,26 +92,28 @@ function renderTree(nodes: TreeNode[], pathname: string) {
<li key={node.slug}> <li key={node.slug}>
<Link <Link
href={`/${node.slug}`} href={`/${node.slug}`}
className={`flex items-center justify-between gap-2 text-xs py-1.5 px-2.5 rounded-lg transition-all ${ className={`flex items-center justify-between gap-2 text-xs py-2 px-3 rounded-xl transition-all ${
isActive isActive
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30" ? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-xs"
: hasActiveChild : hasActiveChild
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
> >
<span className="flex items-center gap-2 truncate"> <span className="flex items-center gap-2.5 truncate">
<span>{isFolder ? "📁" : "📄"}</span> <span className="text-[var(--text-dim)]">
<span className="truncate">{node.doc?.title || node.name}</span> {isFolder ? <Folder className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" /> : <FileText className="w-4 h-4" />}
</span>
<span className="truncate font-medium">{node.doc?.title || node.name}</span>
</span> </span>
{isFolder && ( {isFolder && (
<span className="text-[10px] text-[var(--text-dim)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-1.5 py-0.5 rounded font-mono"> <span className="text-[10px] text-[var(--text-dim)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-2 py-0.5 rounded-md font-mono">
{node.docCount} docs {node.docCount} docs
</span> </span>
)} )}
</Link> </Link>
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className="ml-4 border-l border-[var(--border-color)] pl-2 mt-1"> <div className="ml-4 border-l border-[var(--border-color)] pl-3 mt-1 space-y-1">
{renderTree(node.children, pathname)} {renderTree(node.children, pathname)}
</div> </div>
)} )}
@@ -125,66 +138,74 @@ export default async function SectionIndexPage({
return ( return (
<div className="space-y-10"> <div className="space-y-10">
{/* Breadcrumb */} {/* Breadcrumb Navigation */}
<nav className="flex items-center gap-2 text-xs text-[var(--text-dim)]"> <nav className="flex items-center gap-2 text-xs text-[var(--text-dim)]">
<Link href="/" className="hover:text-[var(--text-primary)] transition-colors"> <Link href="/" className="hover:text-[var(--text-primary)] transition-colors flex items-center gap-1">
MCPedia <span>MCPedia</span>
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-[var(--text-muted)] font-medium">{sectionInfo.label}</span> <span className="text-[var(--text-muted)] font-semibold">{sectionInfo.label}</span>
</nav> </nav>
{/* Hero Card */} {/* Hero Card */}
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 shadow-sm"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-7 shadow-sm">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4 mb-3"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-5 mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-2xl shadow-inner"> <div className="w-14 h-14 rounded-2xl bg-[var(--bg-elevated)] border border-[var(--border-color)] flex items-center justify-center text-3xl shadow-inner">
{sectionInfo.icon} {sectionInfo.icon}
</div> </div>
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight"> <div className="flex items-center gap-2">
<h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
{sectionInfo.label} {sectionInfo.label}
</h1> </h1>
<p className="text-xs text-[var(--text-muted)] mt-0.5 font-mono uppercase"> </div>
Section: {section} <p className="text-xs text-[var(--text-muted)] font-mono uppercase tracking-wider mt-1">
Section Domain: <span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{section}</span>
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap"> <div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 border border-[var(--brand)]/25 px-2.5 py-1 rounded-md"> <span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 border border-[var(--brand)]/25 px-3 py-1.5 rounded-lg font-semibold">
{sectionDocs.length} document{sectionDocs.length !== 1 ? "s" : ""} {sectionDocs.length} document{sectionDocs.length !== 1 ? "s" : ""}
</span> </span>
{sectionDocs.length > 0 && ( {sectionDocs.length > 0 && (
<Link <Link
href={`/${section}/export`} href={`/${section}/export`}
className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)]/10 hover:bg-[var(--brand)]/20 border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 text-[var(--brand)] dark:text-[var(--accent)] px-3 py-1 rounded-md font-medium transition-all shadow-xs" className="inline-flex items-center gap-1.5 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] px-3 py-1.5 rounded-lg font-semibold transition-all shadow-xs"
title={`Export all ${sectionInfo.label} documents to PDF`} title={`Export all ${sectionInfo.label} documents to publication-grade PDF`}
> >
<span>📄</span> <FileDown className="w-3.5 h-3.5" />
<span>Export PDF</span> <span>Export PDF</span>
</Link> </Link>
)} )}
<Link <Link
href="/create" href="/create"
className="inline-flex items-center gap-1 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3 py-1 rounded-md font-medium transition-all" className="inline-flex items-center gap-1.5 text-xs bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white px-3.5 py-1.5 rounded-lg font-semibold transition-all shadow-md shadow-[var(--brand)]/20"
> >
<span>+ Create</span> <Plus className="w-3.5 h-3.5" />
<span>Create Doc</span>
</Link> </Link>
</div> </div>
</div> </div>
<p className="text-sm text-[var(--text-muted)] leading-relaxed max-w-2xl mt-2"> <p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed max-w-2xl mt-3">
{sectionInfo.desc} {sectionInfo.desc}
</p> </p>
</div> </div>
{/* Hierarchical Folder Directory */} {/* Hierarchical Folder Directory */}
{tree.length > 0 && ( {tree.length > 0 && (
<section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-6 shadow-sm"> <section className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 shadow-sm">
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4 flex items-center gap-2"> <div className="flex items-center gap-2 mb-4 pb-2 border-b border-[var(--border-color)]">
<span>Folder Structure</span> <Folder className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
Folder & Chapter Directory
</h2> </h2>
</div>
{renderTree(tree, section)} {renderTree(tree, section)}
</section> </section>
)} )}
@@ -192,10 +213,14 @@ export default async function SectionIndexPage({
{/* Document List */} {/* Document List */}
{sectionDocs.length > 0 ? ( {sectionDocs.length > 0 ? (
<section> <section>
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-4"> <div className="flex items-center gap-2 mb-4">
All Documents in {sectionInfo.label} <FileText className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
All Knowledge in {sectionInfo.label}
</h2> </h2>
<div className="space-y-2"> </div>
<div className="space-y-2.5">
{[...sectionDocs] {[...sectionDocs]
.sort( .sort(
(a, b) => (a, b) =>
@@ -205,20 +230,22 @@ export default async function SectionIndexPage({
<Link <Link
key={d.slug} key={d.slug}
href={`/${d.slug}`} href={`/${d.slug}`}
className="group flex items-center justify-between gap-4 p-4 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl transition-all shadow-sm" className="group flex items-center justify-between gap-4 p-4 sm:p-5 bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3.5 min-w-0">
<span className="text-lg opacity-70 group-hover:opacity-100">📄</span> <div className="p-2 rounded-xl bg-[var(--bg-elevated)] text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors shrink-0">
<FileText className="w-4 h-4" />
</div>
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-medium text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate"> <h3 className="text-sm font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors truncate">
{d.title} {d.title}
</h3> </h3>
{d.tags.length > 0 && ( {d.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1.5"> <div className="flex flex-wrap gap-1.5 mt-2">
{d.tags.slice(0, 4).map((t) => ( {d.tags.slice(0, 4).map((t) => (
<span <span
key={t} key={t}
className="text-[10px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]" className="text-[10px] px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] font-mono"
> >
#{t} #{t}
</span> </span>
@@ -227,19 +254,23 @@ export default async function SectionIndexPage({
)} )}
</div> </div>
</div> </div>
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0">
<time className="text-xs text-[var(--text-dim)] font-mono shrink-0 flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>
{new Date(d.updatedAt).toLocaleDateString(undefined, { {new Date(d.updatedAt).toLocaleDateString(undefined, {
month: "short", month: "short",
day: "numeric", day: "numeric",
year: "numeric", year: "numeric",
})} })}
</span>
</time> </time>
</Link> </Link>
))} ))}
</div> </div>
</section> </section>
) : ( ) : (
<div className="text-center py-12 border border-[var(--border-color)] rounded-xl bg-[var(--bg-surface)]"> <div className="text-center py-16 border border-[var(--border-color)] rounded-2xl bg-[var(--bg-surface)]">
<p className="text-sm text-[var(--text-muted)]">No documents in this section yet.</p> <p className="text-sm text-[var(--text-muted)]">No documents in this section yet.</p>
</div> </div>
)} )}
+151 -49
View File
@@ -3,6 +3,21 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import {
Search,
FileText,
Plus,
Compass,
Sparkles,
ArrowRight,
BookOpen,
Flag,
Layers,
SunMoon,
Command,
X,
Flame,
} from "lucide-react";
interface Hit { interface Hit {
slug: string; slug: string;
@@ -12,6 +27,15 @@ interface Hit {
score: number; score: number;
} }
interface QuickAction {
id: string;
title: string;
subtitle: string;
icon: any;
href?: string;
action?: () => void;
}
export default function CommandMenu() { export default function CommandMenu() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@@ -21,10 +45,48 @@ export default function CommandMenu() {
const router = useRouter(); const router = useRouter();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const quickActions: QuickAction[] = [
{
id: "create-doc",
title: "Create New Document",
subtitle: "Write technical doc, writeup, or research note",
icon: Plus,
href: "/create",
},
{
id: "hybrid-search",
title: "Multi-Modal Hybrid Search",
subtitle: "Full-text + Semantic vector search console",
icon: Search,
href: "/search",
},
{
id: "browse-docs",
title: "Documentation Hub",
subtitle: "System setup, API references, protocol specs",
icon: BookOpen,
href: "/docs",
},
{
id: "browse-writeups",
title: "CTF & Challenge Writeups",
subtitle: "Pwn, Reverse, Web, Crypto writeups and solutions",
icon: Flag,
href: "/writeups",
},
{
id: "browse-research",
title: "Deep-Dive Research",
subtitle: "Architecture notes, benchmarks, experiments",
icon: Sparkles,
href: "/research",
},
];
// Global shortcut ⌘K or / // Global shortcut ⌘K or /
useEffect(() => { useEffect(() => {
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault(); e.preventDefault();
setOpen((prev) => !prev); setOpen((prev) => !prev);
} else if (e.key === "/" && !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName)) { } else if (e.key === "/" && !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName)) {
@@ -56,7 +118,7 @@ export default function CommandMenu() {
const timer = setTimeout(async () => { const timer = setTimeout(async () => {
setLoading(true); setLoading(true);
try { try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=8`); const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&limit=10`);
const data = await res.json(); const data = await res.json();
setResults(data.results || []); setResults(data.results || []);
setSelectedIndex(0); setSelectedIndex(0);
@@ -65,25 +127,31 @@ export default function CommandMenu() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, 150); }, 120);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [query]); }, [query]);
function handleNavigate(slug: string) { function handleNavigate(href: string) {
setOpen(false); setOpen(false);
router.push(`/${slug}`); router.push(href);
} }
const totalItems = query.trim() ? results.length : quickActions.length;
function handleKeyNavigation(e: React.KeyboardEvent) { function handleKeyNavigation(e: React.KeyboardEvent) {
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
setSelectedIndex((prev) => (results.length > 0 ? (prev + 1) % results.length : 0)); setSelectedIndex((prev) => (totalItems > 0 ? (prev + 1) % totalItems : 0));
} else if (e.key === "ArrowUp") { } else if (e.key === "ArrowUp") {
e.preventDefault(); e.preventDefault();
setSelectedIndex((prev) => (results.length > 0 ? (prev - 1 + results.length) % results.length : 0)); setSelectedIndex((prev) => (totalItems > 0 ? (prev - 1 + totalItems) % totalItems : 0));
} else if (e.key === "Enter" && results[selectedIndex]) { } else if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
handleNavigate(results[selectedIndex].slug); if (query.trim() && results[selectedIndex]) {
handleNavigate(`/${results[selectedIndex].slug}`);
} else if (!query.trim() && quickActions[selectedIndex]?.href) {
handleNavigate(quickActions[selectedIndex].href!);
}
} }
} }
@@ -91,13 +159,12 @@ export default function CommandMenu() {
return ( return (
<button <button
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="hidden md:flex items-center gap-2 px-3 py-1.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-sm" type="button"
className="hidden md:flex items-center gap-2.5 px-3 py-1.5 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer group"
> >
<svg className="w-3.5 h-3.5 text-[var(--text-dim)]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Search className="w-3.5 h-3.5 text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <span className="font-sans">Search knowledge...</span>
</svg> <kbd className="ml-2 font-mono bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-dim)] shadow-xs">
<span>Search docs...</span>
<kbd className="ml-2 font-mono bg-[var(--bg-elevated)] px-1.5 py-0.5 rounded border border-[var(--border-color)] text-[10px] text-[var(--text-dim)]">
⌘K ⌘K
</kbd> </kbd>
</button> </button>
@@ -105,87 +172,118 @@ export default function CommandMenu() {
} }
return ( return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-20 px-4"> <div className="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
{/* Backdrop */} {/* Backdrop */}
<div <div
className="fixed inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-sm transition-opacity" className="fixed inset-0 bg-slate-950/60 dark:bg-black/80 backdrop-blur-md transition-opacity"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
/> />
{/* Modal */} {/* Omnisearch Modal */}
<div className="relative w-full max-w-2xl bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-2xl overflow-hidden animate-fade-in"> <div className="relative w-full max-w-2xl bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-2xl overflow-hidden animate-fade-in ring-1 ring-white/10">
{/* Search input bar */} {/* Search input bar */}
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]"> <div className="flex items-center gap-3 px-4 sm:px-5 py-4 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]/70">
<svg className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Search className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] shrink-0" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyNavigation} onKeyDown={handleKeyNavigation}
placeholder="Type to search (titles, keywords, concepts)..." placeholder="Search documents, writeups, concepts, or commands..."
className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm focus:outline-none" className="w-full bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] text-sm sm:text-base focus:outline-none"
/> />
{loading && ( {loading && (
<div className="w-4 h-4 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin shrink-0" /> <div className="w-4 h-4 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin shrink-0" />
)} )}
<button <button
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]" type="button"
className="text-[11px] font-mono text-[var(--text-dim)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] px-2 py-0.5 rounded border border-[var(--border-color)]"
> >
ESC ESC
</button> </button>
</div> </div>
{/* Results / Suggestions */} {/* Results / Quick Actions List */}
<div className="max-h-96 overflow-y-auto p-2"> <div className="max-h-[22rem] overflow-y-auto p-2">
{query.trim() === "" ? ( {query.trim() === "" ? (
<div className="px-3 py-6 text-center"> <div className="p-2 space-y-1">
<p className="text-xs text-[var(--text-muted)]">Search full-text, semantic embeddings, and CTF writeups.</p> <div className="px-3 py-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--text-dim)]">
<div className="flex justify-center gap-2 mt-3 text-[11px] text-[var(--text-dim)]"> Quick Shortcuts & Actions
<span>Navigation: <kbd className="bg-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↑</kbd> <kbd className="bg-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↓</kbd></span>
<span>Select: <kbd className="bg-[var(--bg-elevated)] px-1 py-0.5 rounded border border-[var(--border-color)]">↵</kbd></span>
</div> </div>
{quickActions.map((action, idx) => {
const Icon = action.icon;
const isSelected = idx === selectedIndex;
return (
<div
key={action.id}
onClick={() => action.href && handleNavigate(action.href)}
onMouseEnter={() => setSelectedIndex(idx)}
className={`flex items-center justify-between gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all ${
isSelected
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)] shadow-xs"
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"
}`}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-muted)]"}`}>
<Icon className="w-4 h-4" />
</div>
<div>
<div className="text-xs sm:text-sm font-semibold text-[var(--text-primary)]">
{action.title}
</div>
<div className="text-[11px] text-[var(--text-muted)]">
{action.subtitle}
</div>
</div>
</div>
<ArrowRight className={`w-3.5 h-3.5 text-[var(--text-dim)] ${isSelected ? "text-[var(--brand)] translate-x-0.5" : ""} transition-all`} />
</div>
);
})}
</div> </div>
) : results.length === 0 && !loading ? ( ) : results.length === 0 && !loading ? (
<div className="px-4 py-8 text-center text-xs text-[var(--text-muted)]"> <div className="px-4 py-12 text-center text-xs text-[var(--text-muted)] space-y-2">
No documents matching &quot;{query}&quot; <p>No knowledge base items matched &quot;{query}&quot;</p>
<p className="text-[11px] text-[var(--text-dim)]">
Try searching with partial words or check semantic search
</p>
</div> </div>
) : ( ) : (
<ul className="space-y-1"> <ul className="space-y-1 p-1">
{results.map((hit, index) => { {results.map((hit, index) => {
const isSelected = index === selectedIndex; const isSelected = index === selectedIndex;
return ( return (
<li <li
key={hit.slug} key={hit.slug}
onClick={() => handleNavigate(hit.slug)} onClick={() => handleNavigate(`/${hit.slug}`)}
onMouseEnter={() => setSelectedIndex(index)} onMouseEnter={() => setSelectedIndex(index)}
className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-lg cursor-pointer transition-all ${ className={`flex items-start justify-between gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-all ${
isSelected isSelected
? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)]" ? "bg-[var(--brand)]/15 border border-[var(--brand)]/40 text-[var(--text-primary)] shadow-xs"
: "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent" : "hover:bg-[var(--bg-elevated)] text-[var(--text-secondary)] border border-transparent"
}`} }`}
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 mb-0.5">
<span className="text-xs px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--brand)] dark:text-[var(--accent)] font-mono"> <span className="text-[10px] font-mono px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--brand)] dark:text-[var(--accent)] uppercase font-semibold">
{hit.section} {hit.section}
</span> </span>
<span className={`text-sm font-medium ${isSelected ? "text-[var(--brand)] dark:text-[var(--accent)]" : "text-[var(--text-primary)]"} truncate`}> <span className={`text-xs sm:text-sm font-semibold ${isSelected ? "text-[var(--brand)] dark:text-[var(--accent)]" : "text-[var(--text-primary)]"} truncate`}>
{hit.title} {hit.title}
</span> </span>
</div> </div>
{hit.snippet && ( {hit.snippet && (
<p <p
className="text-xs text-[var(--text-muted)] mt-1 line-clamp-1 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:rounded [&>mark]:px-0.5" className="text-xs text-[var(--text-muted)] line-clamp-1 mt-1 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:rounded [&>mark]:px-0.5"
dangerouslySetInnerHTML={{ __html: hit.snippet }} dangerouslySetInnerHTML={{ __html: hit.snippet }}
/> />
)} )}
</div> </div>
<span className="text-[11px] text-[var(--text-dim)] shrink-0 self-center"> <span className="text-[10px] font-mono text-[var(--text-dim)] shrink-0 self-center">
{hit.slug} /{hit.slug}
</span> </span>
</li> </li>
); );
@@ -195,14 +293,18 @@ export default function CommandMenu() {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-between px-4 py-2.5 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-[11px] text-[var(--text-dim)]"> <div className="flex items-center justify-between px-4 py-2.5 border-t border-[var(--border-color)] bg-[var(--bg-elevated)]/60 text-[11px] text-[var(--text-dim)]">
<span>Tip: Press <kbd className="bg-[var(--bg-surface)] px-1 py-0.5 rounded border border-[var(--border-color)]">Hybrid</kbd> search is enabled</span> <div className="flex items-center gap-2">
<span>Navigate: <kbd className="bg-[var(--bg-surface)] px-1 py-0.5 rounded border border-[var(--border-color)]">↑↓</kbd></span>
<span>Select: <kbd className="bg-[var(--bg-surface)] px-1 py-0.5 rounded border border-[var(--border-color)]">↵</kbd></span>
</div>
<Link <Link
href={`/search?q=${encodeURIComponent(query)}`} href={`/search?q=${encodeURIComponent(query)}`}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] transition-colors" className="text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-medium flex items-center gap-1"
> >
Open full search page → <span>Full Search Console</span>
<ArrowRight className="w-3 h-3" />
</Link> </Link>
</div> </div>
</div> </div>
+23 -27
View File
@@ -2,6 +2,14 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import {
Copy,
Check,
Share2,
FileDown,
Edit3,
ExternalLink,
} from "lucide-react";
interface DocActionsProps { interface DocActionsProps {
slug: string; slug: string;
@@ -27,49 +35,41 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
return ( return (
<div className="flex items-center gap-1.5 shrink-0 flex-wrap"> <div className="flex items-center gap-1.5 shrink-0 flex-wrap">
{/* Copy Markdown */} {/* Copy Raw Markdown */}
<button <button
onClick={handleCopyMarkdown} onClick={handleCopyMarkdown}
type="button" type="button"
title="Copy raw markdown content" title="Copy raw markdown content to clipboard"
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-sm" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer"
> >
{copiedMd ? ( {copiedMd ? (
<> <>
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Check className="w-3.5 h-3.5 text-emerald-500" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-emerald-500 font-medium">Copied MD</span> <span className="text-emerald-500 font-medium">Copied MD</span>
</> </>
) : ( ) : (
<> <>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Copy className="w-3.5 h-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
</svg>
<span>Raw MD</span> <span>Raw MD</span>
</> </>
)} )}
</button> </button>
{/* Copy Link */} {/* Copy Share Link */}
<button <button
onClick={handleCopyLink} onClick={handleCopyLink}
type="button" type="button"
title="Copy link to document" title="Copy link to document"
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-sm" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer"
> >
{copiedLink ? ( {copiedLink ? (
<> <>
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Check className="w-3.5 h-3.5 text-emerald-500" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-emerald-500 font-medium">Copied Link</span> <span className="text-emerald-500 font-medium">Copied Link</span>
</> </>
) : ( ) : (
<> <>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Share2 className="w-3.5 h-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span>Share</span> <span>Share</span>
</> </>
)} )}
@@ -78,23 +78,19 @@ export default function DocActions({ slug, body, canEdit }: DocActionsProps) {
{/* Export PDF */} {/* Export PDF */}
<Link <Link
href={`/${slug}/export`} href={`/${slug}/export`}
title="Export document or parent folder to PDF" title="Export document or parent folder to publication-grade PDF"
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-sm" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs"
> >
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <FileDown className="w-3.5 h-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" /> <span>Export PDF</span>
</svg>
<span>PDF</span>
</Link> </Link>
{/* Edit Link */} {/* Edit Link */}
<Link <Link
href={canEdit ? `/${slug}?edit=1` : `/login?redirect=/${slug}`} href={canEdit ? `/${slug}?edit=1` : `/login?redirect=/${slug}`}
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs bg-[var(--brand)]/10 hover:bg-[var(--brand)]/20 border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 text-[var(--brand)] dark:text-[var(--accent)] rounded-md transition-all font-medium" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[var(--brand)]/10 hover:bg-[var(--brand)]/20 border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 text-[var(--brand)] dark:text-[var(--accent)] rounded-lg transition-all font-medium shadow-xs"
> >
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Edit3 className="w-3.5 h-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span>Edit</span> <span>Edit</span>
</Link> </Link>
</div> </div>
+136 -77
View File
@@ -3,6 +3,27 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Markdown from "./Markdown"; import Markdown from "./Markdown";
import {
Bold,
Italic,
Heading2,
Heading3,
Code,
Link as LinkIcon,
Table as TableIcon,
Info,
Lightbulb,
AlertTriangle,
Plus,
Trash2,
Check,
Eye,
Edit3,
FileCode,
Tag,
Folder,
Sliders,
} from "lucide-react";
interface DocFormProps { interface DocFormProps {
mode: "create" | "edit"; mode: "create" | "edit";
@@ -22,8 +43,26 @@ interface DocFormProps {
}; };
} }
const DEFAULT_SECTION_LIST = ["docs", "writeups", "research", "notes", "guides", "tutorials", "ctf", "api", "projects"]; const DEFAULT_SECTION_LIST = [
const DEFAULT_TYPE_LIST = ["documentation", "writeup", "research", "note", "guide", "tutorial", "spec"]; "docs",
"writeups",
"research",
"notes",
"guides",
"tutorials",
"ctf",
"api",
"projects",
];
const DEFAULT_TYPE_LIST = [
"documentation",
"writeup",
"research",
"note",
"guide",
"tutorial",
"spec",
];
const PRESET_FIELDS = [ const PRESET_FIELDS = [
{ key: "difficulty", value: "medium" }, { key: "difficulty", value: "medium" },
@@ -88,7 +127,7 @@ export default function DocForm({
const availableFolders = existingFolders?.[section] ?? []; const availableFolders = existingFolders?.[section] ?? [];
const baseInputCls = const baseInputCls =
"w-full px-3.5 py-2 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[var(--brand)] transition-colors"; "w-full px-3.5 py-2.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 transition-all";
function addCustomField(key = "", value = "") { function addCustomField(key = "", value = "") {
setCustomFields([...customFields, { key, value }]); setCustomFields([...customFields, { key, value }]);
@@ -128,7 +167,7 @@ export default function DocForm({
setError(null); setError(null);
if (!effectiveSlug || !title || !body) { if (!effectiveSlug || !title || !body) {
setError("Slug, title, and body are required."); setError("Slug, title, and markdown body are required.");
setLoading(false); setLoading(false);
return; return;
} }
@@ -175,7 +214,7 @@ export default function DocForm({
if (!res.ok) { if (!res.ok) {
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
throw new Error(data.error ?? "Save failed"); throw new Error(data.error ?? "Failed to save document. Verify ADMIN_PASSWORD or secret.");
} }
const result = await res.json(); const result = await res.json();
@@ -190,39 +229,38 @@ export default function DocForm({
return ( return (
<form onSubmit={handleSubmit} className="space-y-6 max-w-4xl"> <form onSubmit={handleSubmit} className="space-y-6 max-w-4xl">
{error && ( {error && (
<div className="p-3.5 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-sm"> <div className="p-4 rounded-xl border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs sm:text-sm font-medium animate-fade-in">
{error} {error}
</div> </div>
)} )}
{/* Title */} {/* Document Title */}
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Document Title * Document Title *
</label> </label>
<input <input
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="e.g. Database Architecture Overview" placeholder="e.g. Exploiting Stack Alignment Issues in x86_64"
className={`${baseInputCls} text-base`} className={`${baseInputCls} text-base sm:text-lg font-semibold`}
required required
/> />
</div> </div>
{/* Section + Type + Status */} {/* Section, Type, Status */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Section Section
</label> </label>
<div className="relative">
<input <input
type="text" type="text"
list="section-list" list="section-list"
value={section} value={section}
onChange={(e) => setSection(e.target.value)} onChange={(e) => setSection(e.target.value)}
placeholder="e.g. docs, writeups, ctf, guides" placeholder="e.g. docs, writeups, research"
className={baseInputCls} className={baseInputCls}
required required
/> />
@@ -234,18 +272,17 @@ export default function DocForm({
))} ))}
</datalist> </datalist>
</div> </div>
</div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Type Document Type
</label> </label>
<div className="relative">
<input <input
type="text" type="text"
list="type-list" list="type-list"
value={type} value={type}
onChange={(e) => setType(e.target.value)} onChange={(e) => setType(e.target.value)}
placeholder="e.g. documentation, writeup, note" placeholder="e.g. writeup, documentation, note"
className={baseInputCls} className={baseInputCls}
/> />
<datalist id="type-list"> <datalist id="type-list">
@@ -256,9 +293,9 @@ export default function DocForm({
))} ))}
</datalist> </datalist>
</div> </div>
</div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Status Status
</label> </label>
<select <select
@@ -267,7 +304,7 @@ export default function DocForm({
className={baseInputCls} className={baseInputCls}
> >
<option value="published">Published</option> <option value="published">Published</option>
<option value="draft">Draft</option> <option value="draft">Draft (Unindexed)</option>
</select> </select>
</div> </div>
</div> </div>
@@ -275,8 +312,8 @@ export default function DocForm({
{/* Folder + Slug */} {/* Folder + Slug */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Parent Folder Parent Folder / Chapter
</label> </label>
<select <select
value={parentFolder} value={parentFolder}
@@ -292,16 +329,17 @@ export default function DocForm({
))} ))}
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
{isEdit ? "Full Slug" : "Slug (File name) *"} {isEdit ? "Document Path" : "Slug (File name) *"}
</label> </label>
{isEdit ? ( {isEdit ? (
<input <input
type="text" type="text"
value={slug ?? ""} value={slug ?? ""}
readOnly readOnly
className="w-full px-3.5 py-2 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg text-sm text-[var(--text-muted)] font-mono" className="w-full px-3.5 py-2.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-xl text-sm text-[var(--text-muted)] font-mono"
/> />
) : ( ) : (
<input <input
@@ -309,12 +347,12 @@ export default function DocForm({
value={slugInput} value={slugInput}
onChange={(e) => setSlugInput(e.target.value)} onChange={(e) => setSlugInput(e.target.value)}
className={`${baseInputCls} font-mono`} className={`${baseInputCls} font-mono`}
placeholder="my-new-post" placeholder="ret2win-guide"
required required
/> />
)} )}
<p className="text-[11px] text-[var(--text-dim)] mt-1 font-mono"> <p className="text-[11px] text-[var(--text-dim)] mt-1.5 font-mono">
Path: <span className="text-[var(--brand)] dark:text-[var(--accent)]">{effectiveSlug || section}</span> Full Slug: <span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">{effectiveSlug || section}</span>
</p> </p>
</div> </div>
</div> </div>
@@ -322,7 +360,7 @@ export default function DocForm({
{/* Tags + Author */} {/* Tags + Author */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Tags (comma separated) Tags (comma separated)
</label> </label>
<input <input
@@ -330,11 +368,11 @@ export default function DocForm({
value={tags} value={tags}
onChange={(e) => setTags(e.target.value)} onChange={(e) => setTags(e.target.value)}
className={baseInputCls} className={baseInputCls}
placeholder="ctf, pwn, reverse, tutorial" placeholder="pwn, x86_64, rop, ctf"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Author Author
</label> </label>
<input <input
@@ -342,31 +380,32 @@ export default function DocForm({
value={author} value={author}
onChange={(e) => setAuthor(e.target.value)} onChange={(e) => setAuthor(e.target.value)}
className={baseInputCls} className={baseInputCls}
placeholder="asep" placeholder="e.g. asepharyana"
/> />
</div> </div>
</div> </div>
{/* Custom Metadata Fields */} {/* Custom Metadata Fields */}
<div className="p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-3 shadow-sm"> <div className="p-4 sm:p-5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl space-y-4 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div> <div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)]"> <h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] font-mono flex items-center gap-1.5">
Dynamic Custom Metadata <Sliders className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
<span>Dynamic Technical Metadata</span>
</h3> </h3>
<p className="text-[11px] text-[var(--text-dim)] mt-0.5"> <p className="text-[11px] text-[var(--text-dim)] mt-0.5">
Add custom key-values (event, difficulty, points, etc.) rendered as styled badges. Custom fields (points, difficulty, event, solved) will render as styled badges in the document hero.
</p> </p>
</div> </div>
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[11px] text-[var(--text-dim)]">Presets:</span> <span className="text-[10px] font-mono text-[var(--text-dim)] uppercase">Presets:</span>
{PRESET_FIELDS.map((preset) => ( {PRESET_FIELDS.map((preset) => (
<button <button
key={preset.key} key={preset.key}
type="button" type="button"
onClick={() => addCustomField(preset.key, preset.value)} onClick={() => addCustomField(preset.key, preset.value)}
className="px-2 py-0.5 text-[11px] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded transition-colors" className="px-2 py-0.5 text-[11px] bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-md transition-colors font-mono cursor-pointer"
> >
+{preset.key} +{preset.key}
</button> </button>
@@ -393,10 +432,10 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => removeCustomField(i)} onClick={() => removeCustomField(i)}
className="p-2 text-rose-500 hover:text-rose-600 rounded hover:bg-[var(--bg-elevated)] transition-colors" className="p-2 text-rose-500 hover:text-rose-600 rounded-lg hover:bg-[var(--bg-elevated)] transition-colors cursor-pointer"
title="Remove field" title="Remove field"
> >
✕ <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
))} ))}
@@ -404,105 +443,124 @@ export default function DocForm({
<button <button
type="button" type="button"
onClick={() => addCustomField()} onClick={() => addCustomField()}
className="text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline px-3 py-1.5 rounded-lg border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 bg-[var(--brand)]/5 transition-colors font-medium" className="inline-flex items-center gap-1.5 text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline px-3 py-1.5 rounded-lg border border-[var(--brand)]/30 hover:border-[var(--brand)]/60 bg-[var(--brand)]/5 transition-colors font-medium cursor-pointer"
> >
+ Add Custom Field <Plus className="w-3.5 h-3.5" />
<span>Add Custom Field</span>
</button> </button>
</div> </div>
{/* Body with Write / Preview Tabs */} {/* Markdown Body Editor */}
<div className="border border-[var(--border-color)] rounded-xl overflow-hidden bg-[var(--bg-surface)] shadow-sm"> <div className="border border-[var(--border-color)] rounded-2xl overflow-hidden bg-[var(--bg-surface)] shadow-sm">
{/* Editor Toolbar */} {/* Editor Toolbar */}
<div className="flex items-center justify-between px-3 py-2 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2"> <div className="flex items-center justify-between px-3 py-2 bg-[var(--bg-elevated)]/60 border-b border-[var(--border-color)] flex-wrap gap-2">
{/* Tabs */} {/* Tabs */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button <button
type="button" type="button"
onClick={() => setActiveTab("write")} onClick={() => setActiveTab("write")}
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-semibold transition-all cursor-pointer ${
activeTab === "write" activeTab === "write"
? "bg-[var(--brand)] text-white shadow-sm" ? "bg-[var(--brand)] text-white shadow-xs"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
Write <Edit3 className="w-3.5 h-3.5" />
<span>Write</span>
</button> </button>
<button <button
type="button" type="button"
onClick={() => setActiveTab("preview")} onClick={() => setActiveTab("preview")}
className={`px-3 py-1 text-xs rounded-md font-medium transition-all ${ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg font-semibold transition-all cursor-pointer ${
activeTab === "preview" activeTab === "preview"
? "bg-[var(--brand)] text-white shadow-sm" ? "bg-[var(--brand)] text-white shadow-xs"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
Live Preview <Eye className="w-3.5 h-3.5" />
<span>Live Preview</span>
</button> </button>
</div> </div>
{/* Markdown Action Shortcuts */} {/* Quick Markdown Formatter Shortcuts */}
{activeTab === "write" && ( {activeTab === "write" && (
<div className="flex items-center gap-1 text-xs text-[var(--text-muted)]"> <div className="flex items-center gap-1 text-xs text-[var(--text-muted)]">
<button <button
type="button" type="button"
onClick={() => insertMarkdown("**", "**")} onClick={() => insertMarkdown("**", "**")}
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]" className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
title="Bold" title="Bold (**text**)"
> >
<b>B</b> <Bold className="w-3.5 h-3.5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => insertMarkdown("*", "*")} onClick={() => insertMarkdown("*", "*")}
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]" className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
title="Italic" title="Italic (*text*)"
> >
<i>I</i> <Italic className="w-3.5 h-3.5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => insertMarkdown("## ")} onClick={() => insertMarkdown("## ")}
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]" className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
title="Heading 2" title="Heading 2"
> >
H2 <Heading2 className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown("### ")}
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
title="Heading 3"
>
<Heading3 className="w-3.5 h-3.5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => insertMarkdown("```\n", "\n```")} onClick={() => insertMarkdown("```\n", "\n```")}
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] font-mono" className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
title="Code Block" title="Code Block"
> >
{"</>"} <Code className="w-3.5 h-3.5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => insertMarkdown("[", "](https://)")} onClick={() => insertMarkdown("[", "](https://)")}
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]" className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)]"
title="Link" title="Link"
> >
Link <LinkIcon className="w-3.5 h-3.5" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => insertMarkdown("> [!NOTE]\n> ")} onClick={() => insertMarkdown("> [!NOTE]\n> ")}
className="px-2 py-0.5 hover:bg-[var(--bg-surface)] rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)]" className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)] text-indigo-500"
title="Note Callout" title="Note Callout"
> >
Note <Info className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => insertMarkdown("> [!TIP]\n> ")}
className="p-1.5 hover:bg-[var(--bg-surface)] rounded-md hover:text-[var(--text-primary)] text-emerald-500"
title="Tip Callout"
>
<Lightbulb className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
)} )}
</div> </div>
{/* Tab content */} {/* Tab Content */}
{activeTab === "write" ? ( {activeTab === "write" ? (
<textarea <textarea
ref={textareaRef} ref={textareaRef}
value={body} value={body}
onChange={(e) => setBody(e.target.value)} onChange={(e) => setBody(e.target.value)}
className="w-full h-96 p-4 bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] font-mono text-sm focus:outline-none resize-y leading-relaxed" className="w-full h-96 p-4 sm:p-5 bg-transparent text-[var(--text-primary)] placeholder-[var(--text-dim)] font-mono text-sm focus:outline-none resize-y leading-relaxed"
placeholder="# Write your markdown document here..." placeholder="# Write your technical documentation or writeup here in Markdown..."
required required
/> />
) : ( ) : (
@@ -510,28 +568,29 @@ export default function DocForm({
{body.trim() ? ( {body.trim() ? (
<Markdown source={body} /> <Markdown source={body} />
) : ( ) : (
<p className="text-xs text-[var(--text-dim)] italic">Nothing to preview yet.</p> <p className="text-xs text-[var(--text-dim)] italic">No content to preview yet.</p>
)} )}
</div> </div>
)} )}
</div> </div>
{/* Actions */} {/* Submit Action Buttons */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="px-6 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg font-medium text-sm transition-all shadow-md shadow-[var(--brand)]/20 disabled:opacity-50 flex items-center gap-2" className="px-6 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-xl font-semibold text-sm transition-all shadow-md shadow-[var(--brand)]/20 hover:shadow-[var(--brand)]/35 disabled:opacity-50 flex items-center gap-2 cursor-pointer"
> >
{loading && ( {loading && (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
)} )}
<span>{loading ? "Saving document..." : mode === "create" ? "Create Document" : "Save Changes"}</span> <span>{loading ? "Persisting Document..." : mode === "create" ? "Publish to PostgreSQL" : "Save Changes"}</span>
</button> </button>
<button <button
type="button" type="button"
onClick={() => router.back()} onClick={() => router.back()}
className="px-4 py-2.5 border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] rounded-lg text-sm transition-colors shadow-sm" className="px-4 py-2.5 border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] bg-[var(--bg-surface)] rounded-xl text-sm transition-colors shadow-xs cursor-pointer"
> >
Cancel Cancel
</button> </button>
+78 -49
View File
@@ -6,6 +6,18 @@ import { usePathname } from "next/navigation";
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections"; import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
import ThemeToggle from "@/components/ThemeToggle"; import ThemeToggle from "@/components/ThemeToggle";
import CommandMenu from "@/components/CommandMenu"; import CommandMenu from "@/components/CommandMenu";
import {
Sparkles,
Plus,
Search,
Menu,
X,
Lock,
Layers,
FileText,
Activity,
Terminal,
} from "lucide-react";
export default function Header() { export default function Header() {
const pathname = usePathname(); const pathname = usePathname();
@@ -24,126 +36,143 @@ export default function Header() {
}, []); }, []);
return ( return (
<header className="sticky top-0 z-30 border-b border-[var(--border-color)] glass-nav"> <header className="sticky top-0 z-40 border-b border-[var(--border-color)] glass-nav transition-all">
<div className="mx-auto max-w-7xl px-4 sm:px-6 h-14 flex items-center justify-between gap-4"> <div className="mx-auto max-w-7xl px-4 sm:px-6 h-15 flex items-center justify-between gap-3 sm:gap-4">
{/* Brand */} {/* Left: Brand Identity */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/" className="flex items-center gap-2 group"> <Link href="/" className="flex items-center gap-2.5 group">
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-[var(--brand)] to-[var(--accent)] flex items-center justify-center text-white font-bold text-xs shadow-md shadow-[var(--brand)]/20 group-hover:scale-105 transition-transform"> <div className="relative flex items-center justify-center w-8 h-8 rounded-xl bg-gradient-to-br from-indigo-500 via-indigo-600 to-violet-700 text-white shadow-lg shadow-indigo-500/25 group-hover:scale-105 group-hover:shadow-indigo-500/40 transition-all">
M <span className="font-mono font-bold text-sm tracking-tighter">M</span>
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-400 ring-2 ring-[var(--bg-app)] animate-pulse" />
</div> </div>
<span className="font-semibold text-[var(--text-primary)] text-base tracking-tight transition-colors"> <div className="flex flex-col">
<span className="font-bold text-[var(--text-primary)] text-base tracking-tight leading-none group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
MCPedia MCPedia
</span> </span>
</Link> <span className="text-[10px] font-mono text-[var(--text-dim)] uppercase tracking-wider leading-tight mt-0.5 hidden sm:block">
<span className="hidden sm:inline-block text-[10px] uppercase font-mono px-1.5 py-0.5 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)]"> AI Knowledge Hub
v0.1.0
</span> </span>
</div> </div>
</Link>
{/* Desktop Nav */} <div className="hidden xl:flex items-center gap-1.5 ml-2 px-2 py-0.5 rounded-full bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[10px] font-mono text-[var(--text-dim)]">
<nav className="hidden md:flex items-center gap-1"> <Activity className="w-3 h-3 text-emerald-500" />
<span>MCP :4021</span>
</div>
</div>
{/* Center: Desktop Dynamic Sections Nav */}
<nav className="hidden md:flex items-center gap-1 bg-[var(--bg-elevated)]/60 p-1 rounded-xl border border-[var(--border-color)]">
{sections.map((s) => { {sections.map((s) => {
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`); const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
return ( return (
<Link <Link
key={s.id} key={s.id}
href={`/${s.id}`} href={`/${s.id}`}
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all ${ className={`px-3 py-1 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5 ${
isActive isActive
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] border border-[var(--border-color)]" ? "text-[var(--text-primary)] bg-[var(--bg-surface)] shadow-xs font-semibold border border-[var(--border-color)]"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface-hover)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]/50"
}`} }`}
> >
<span className="mr-1.5 opacity-80">{s.icon}</span> <span className="text-xs">{s.icon}</span>
{s.label} <span>{s.label}</span>
</Link> </Link>
); );
})} })}
</nav> </nav>
{/* Right tools */} {/* Right Tools & Navigation Deck */}
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2">
<CommandMenu /> <CommandMenu />
<Link <Link
href="/search" href="/search"
className="md:hidden p-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-elevated)] transition-colors" className="md:hidden p-2 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg bg-[var(--bg-surface)] border border-[var(--border-color)] transition-colors"
title="Search" title="Search knowledge base"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Search className="w-4 h-4" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</Link> </Link>
<Link <Link
href="/create" href="/create"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1 bg-[var(--brand)] hover:bg-[var(--accent)] text-white text-xs font-medium rounded-lg transition-all shadow-sm" className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white text-xs font-medium rounded-lg shadow-md shadow-[var(--brand)]/20 hover:shadow-[var(--brand)]/35 transition-all cursor-pointer"
> >
<span>+</span> <Plus className="w-3.5 h-3.5" />
<span>New Doc</span> <span>New Doc</span>
</Link> </Link>
<Link <Link
href="/login" href="/login"
className={`px-2.5 py-1 text-xs rounded-lg transition-colors ${ className={`p-1.5 sm:px-2.5 sm:py-1.5 text-xs rounded-lg transition-colors flex items-center gap-1.5 ${
pathname === "/login" pathname === "/login"
? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-medium" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)] font-semibold border border-[var(--border-color)]"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
title="Admin Login"
> >
Login <Lock className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Admin</span>
</Link> </Link>
<div className="border-l border-[var(--border-color)] pl-2.5 flex items-center"> <div className="border-l border-[var(--border-color)] pl-2 flex items-center">
<ThemeToggle /> <ThemeToggle />
</div> </div>
{/* Mobile menu toggle */} {/* Mobile menu hamburger toggle */}
<button <button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)]" className="md:hidden p-2 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-elevated)] transition-colors"
aria-label="Toggle navigation" aria-label="Toggle navigation"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> {mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button> </button>
</div> </div>
</div> </div>
{/* Mobile Drawer */} {/* Mobile Drawer */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden border-t border-[var(--border-color)] bg-[var(--bg-surface)] px-4 py-3 space-y-1 animate-fade-in shadow-lg"> <div className="md:hidden border-t border-[var(--border-color)] bg-[var(--bg-surface)] px-4 py-4 space-y-2 animate-fade-in shadow-xl">
{sections.map((s) => ( <div className="text-[11px] font-mono uppercase tracking-wider text-[var(--text-dim)] px-2 mb-1">
Sections
</div>
<div className="grid grid-cols-2 gap-1.5">
{sections.map((s) => {
const isActive = pathname === `/${s.id}` || pathname.startsWith(`/${s.id}/`);
return (
<Link <Link
key={s.id} key={s.id}
href={`/${s.id}`} href={`/${s.id}`}
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text-primary)]" className={`flex items-center gap-2 px-3 py-2 rounded-lg text-xs font-medium transition-colors ${
isActive
? "bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30 font-semibold"
: "text-[var(--text-secondary)] hover:bg-[var(--bg-elevated)] hover:text-[var(--text-primary)] border border-transparent"
}`}
> >
<span>{s.icon}</span> <span>{s.icon}</span>
<span>{s.label}</span> <span className="truncate">{s.label}</span>
</Link> </Link>
))} );
<div className="pt-2 border-t border-[var(--border-color)] flex gap-2"> })}
</div>
<div className="pt-3 border-t border-[var(--border-color)] flex gap-2">
<Link <Link
href="/search" href="/search"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex-1 text-center py-2 text-xs bg-[var(--bg-elevated)] text-[var(--text-secondary)] rounded-lg font-medium border border-[var(--border-color)]" className="flex-1 flex items-center justify-center gap-1.5 py-2 text-xs bg-[var(--bg-elevated)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg font-medium border border-[var(--border-color)]"
> >
Search <Search className="w-3.5 h-3.5" />
<span>Search</span>
</Link> </Link>
<Link <Link
href="/create" href="/create"
onClick={() => setMobileMenuOpen(false)} onClick={() => setMobileMenuOpen(false)}
className="flex-1 text-center py-2 text-xs bg-[var(--brand)] text-white rounded-lg font-medium" className="flex-1 flex items-center justify-center gap-1.5 py-2 text-xs bg-[var(--brand)] text-white rounded-lg font-medium shadow-sm"
> >
+ Create <Plus className="w-3.5 h-3.5" />
<span>Create Doc</span>
</Link> </Link>
</div> </div>
</div> </div>
@@ -0,0 +1,379 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import {
Cpu,
Database,
Layers,
Sparkles,
Search,
Globe,
Terminal,
Server,
ArrowRight,
CheckCircle2,
FileCode2,
ShieldCheck,
} from "lucide-react";
interface NodeDetail {
id: string;
title: string;
badge: string;
type: "client" | "protocol" | "core" | "storage";
icon: any;
desc: string;
metrics: { label: string; value: string }[];
features: string[];
docLink?: string;
}
const NODES: NodeDetail[] = [
{
id: "ai-agents",
title: "AI Coding Agents",
badge: "MCP Consumer",
type: "client",
icon: Sparkles,
desc: "Claude Desktop, Cursor, Antigravity, and Zed connect via Model Context Protocol to read and write technical knowledge directly.",
metrics: [
{ label: "Transport", value: "Stdio / HTTP SSE" },
{ label: "Tools Exposed", value: "13 Native Tools" },
{ label: "Authentication", value: "x-webhook-secret" },
],
features: [
"Semantic similarity search over chunks",
"Direct document CRUD into Postgres",
"Context retrieval for debugging & CTF writeups",
],
docLink: "/docs",
},
{
id: "web-ui",
title: "Human Web Interface",
badge: "Next.js 16 + React 19",
type: "client",
icon: Globe,
desc: "Fast, accessible, and responsive developer web interface with dark/light themes, keyboard shortcuts, and instant search.",
metrics: [
{ label: "Framework", value: "Next.js 16.3" },
{ label: "Styling", value: "Tailwind CSS v4" },
{ label: "Command Menu", value: "⌘K Omnisearch" },
],
features: [
"Hierarchical folder directory tree",
"Publication-grade PDF export generator",
"Live markdown editor with revision rollback",
],
docLink: "/docs",
},
{
id: "mcp-server",
title: "MCP Server Gateway",
badge: "Streamable HTTP :4021",
type: "protocol",
icon: Server,
desc: "High-throughput streamable Model Context Protocol server exposing tool endpoints and resource URIs.",
metrics: [
{ label: "Port", value: ":4021" },
{ label: "Protocol", value: "MCP 2024-11-05" },
{ label: "Runtime", value: "Bun Engine" },
],
features: [
"get_document / create_document tools",
"search_hybrid / search_semantic tools",
"list_sections & list_revisions resources",
],
docLink: "/api",
},
{
id: "core-engine",
title: "@mcpedia/core",
badge: "Single Source of Truth",
type: "core",
icon: Layers,
desc: "Unified business logic layer shared across Web, MCP Server, REST API, and Background Workers.",
metrics: [
{ label: "Architecture", value: "Monorepo Package" },
{ label: "Validation", value: "Strict Type Safety" },
{ label: "Ranking", value: "RRF Hybrid Fusion" },
],
features: [
"Path classification & folder hierarchy",
"Markdown document chunking & tokenization",
"Drizzle ORM transactional schema mapping",
],
docLink: "/research",
},
{
id: "search-engine",
title: "Multi-Modal Search Engine",
badge: "RRF Algorithm",
type: "core",
icon: Search,
desc: "Reciprocal Rank Fusion uniting Postgres Full-Text Search tsvectors with OpenAI/OpenRouter vector embeddings.",
metrics: [
{ label: "FTS Algorithm", value: "tsvector + GIN" },
{ label: "Vector Search", value: "Cosine Similarity" },
{ label: "Fusion Constant", value: "k = 60" },
],
features: [
"Sub-millisecond keyword retrieval",
"Conceptual similarity matching",
"Instant snippet generation with highlight markers",
],
docLink: "/search",
},
{
id: "postgres-db",
title: "PostgreSQL 16 + pgvector",
badge: "Primary Storage",
type: "storage",
icon: Database,
desc: "ACID-compliant relational database storing document bodies, revision trees, vector embeddings, and full-text indexes.",
metrics: [
{ label: "Extension", value: "vector (1536 dim)" },
{ label: "Backups", value: "Auto .md to disk" },
{ label: "Isolation", value: "Transactional" },
],
features: [
"HNSW vector index for embeddings",
"Complete revision history preservation",
"Instant document schema migrations",
],
docLink: "/docs",
},
];
export default function InteractiveTopology() {
const [activeNodeId, setActiveNodeId] = useState<string>("core-engine");
const activeNode = NODES.find((n) => n.id === activeNodeId) || NODES[0];
const ActiveIcon = activeNode.icon;
return (
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl overflow-hidden shadow-sm">
{/* Header Bar */}
<div className="p-5 sm:p-6 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]/50 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[11px] font-mono uppercase tracking-wider text-[var(--text-muted)]">
Interactive System Topology
</span>
</div>
<h3 className="text-xl font-bold text-[var(--text-primary)] tracking-tight">
How MCPedia Unifies Humans & AI Agents
</h3>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-2.5 py-1 rounded-md">
Click any node below to inspect
</span>
</div>
</div>
{/* Grid Layout: Visual Interactive Map & Inspector */}
<div className="grid grid-cols-1 lg:grid-cols-12">
{/* Visual Map Column (Left) */}
<div className="lg:col-span-7 p-6 border-b lg:border-b-0 lg:border-r border-[var(--border-color)] bg-[var(--bg-app)]/50 flex flex-col justify-between space-y-6">
{/* Layer 1: Clients */}
<div>
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
1. Ingestion & Access Layer
</span>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{NODES.filter((n) => n.type === "client").map((node) => {
const Icon = node.icon;
const isSelected = activeNodeId === node.id;
return (
<button
key={node.id}
onClick={() => setActiveNodeId(node.id)}
type="button"
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
isSelected
? "bg-[var(--brand)]/15 border-[var(--brand)] shadow-md shadow-[var(--brand)]/10 ring-1 ring-[var(--brand)]"
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--brand)]/50 hover:bg-[var(--bg-elevated)]"
}`}
>
<div className="flex items-center justify-between mb-2">
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}>
<Icon className="w-4 h-4" />
</div>
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-[var(--bg-elevated)] text-[var(--text-muted)] border border-[var(--border-color)]">
{node.badge}
</span>
</div>
<div className="font-semibold text-sm text-[var(--text-primary)]">{node.title}</div>
</button>
);
})}
</div>
</div>
{/* Flow Connector Arrow */}
<div className="flex justify-center text-[var(--text-dim)]">
<span className="text-xs font-mono px-3 py-0.5 rounded-full bg-[var(--bg-surface)] border border-[var(--border-color)]">
↓ Secure Protocol & Gateway
</span>
</div>
{/* Layer 2: Protocol & Core Services */}
<div>
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
2. Gateway & Intelligence Core
</span>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{NODES.filter((n) => n.type === "protocol" || n.type === "core").map((node) => {
const Icon = node.icon;
const isSelected = activeNodeId === node.id;
return (
<button
key={node.id}
onClick={() => setActiveNodeId(node.id)}
type="button"
className={`p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
isSelected
? "bg-[var(--brand)]/15 border-[var(--brand)] shadow-md shadow-[var(--brand)]/10 ring-1 ring-[var(--brand)]"
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--brand)]/50 hover:bg-[var(--bg-elevated)]"
}`}
>
<div className="flex items-center justify-between mb-2">
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}>
<Icon className="w-4 h-4" />
</div>
</div>
<div className="font-semibold text-xs text-[var(--text-primary)] leading-tight mb-1 truncate">
{node.title}
</div>
<div className="text-[10px] text-[var(--text-muted)] font-mono truncate">
{node.badge}
</div>
</button>
);
})}
</div>
</div>
{/* Flow Connector Arrow */}
<div className="flex justify-center text-[var(--text-dim)]">
<span className="text-xs font-mono px-3 py-0.5 rounded-full bg-[var(--bg-surface)] border border-[var(--border-color)]">
↓ Persistent Layer (ACID + Vectors)
</span>
</div>
{/* Layer 3: Database & Vector Storage */}
<div>
<span className="text-[10px] font-mono uppercase tracking-widest text-[var(--text-dim)] mb-2 block">
3. Source of Truth Storage
</span>
{NODES.filter((n) => n.type === "storage").map((node) => {
const Icon = node.icon;
const isSelected = activeNodeId === node.id;
return (
<button
key={node.id}
onClick={() => setActiveNodeId(node.id)}
type="button"
className={`w-full p-3.5 rounded-xl border text-left transition-all cursor-pointer ${
isSelected
? "bg-[var(--brand)]/15 border-[var(--brand)] shadow-md shadow-[var(--brand)]/10 ring-1 ring-[var(--brand)]"
: "bg-[var(--bg-surface)] border-[var(--border-color)] hover:border-[var(--brand)]/50 hover:bg-[var(--bg-elevated)]"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${isSelected ? "bg-[var(--brand)] text-white" : "bg-[var(--bg-elevated)] text-[var(--text-secondary)]"}`}>
<Icon className="w-4 h-4" />
</div>
<div>
<div className="font-semibold text-sm text-[var(--text-primary)]">{node.title}</div>
<div className="text-xs text-[var(--text-muted)]">Source of truth for all content & embeddings</div>
</div>
</div>
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-[var(--bg-elevated)] text-[var(--text-muted)] border border-[var(--border-color)]">
{node.badge}
</span>
</div>
</button>
);
})}
</div>
</div>
{/* Node Inspector Details Column (Right) */}
<div className="lg:col-span-5 p-6 bg-[var(--bg-surface)] flex flex-col justify-between space-y-6 animate-fade-in">
<div>
{/* Inspector Header */}
<div className="flex items-start justify-between gap-4 mb-4">
<div className="flex items-center gap-3">
<div className="w-11 h-11 rounded-xl bg-[var(--brand)]/15 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] flex items-center justify-center">
<ActiveIcon className="w-5 h-5" />
</div>
<div>
<h4 className="text-lg font-bold text-[var(--text-primary)]">{activeNode.title}</h4>
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)]">
{activeNode.badge}
</span>
</div>
</div>
</div>
<p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed mb-6">
{activeNode.desc}
</p>
{/* Telemetry Metrics */}
<div className="mb-6">
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-dim)] font-mono mb-2.5 block">
Telemetry & Architecture
</span>
<div className="space-y-2">
{activeNode.metrics.map((m) => (
<div
key={m.label}
className="flex items-center justify-between p-2.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg text-xs"
>
<span className="text-[var(--text-muted)]">{m.label}</span>
<span className="font-mono font-medium text-[var(--text-primary)]">{m.value}</span>
</div>
))}
</div>
</div>
{/* Key Capabilities */}
<div>
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-dim)] font-mono mb-2.5 block">
Key Capabilities
</span>
<ul className="space-y-2">
{activeNode.features.map((feat, idx) => (
<li key={idx} className="flex items-start gap-2 text-xs text-[var(--text-secondary)]">
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-500 shrink-0 mt-0.5" />
<span>{feat}</span>
</li>
))}
</ul>
</div>
</div>
{/* Link to Documentation */}
{activeNode.docLink && (
<div className="pt-4 border-t border-[var(--border-color)]">
<Link
href={activeNode.docLink}
className="inline-flex items-center justify-center gap-2 w-full py-2.5 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-xs font-semibold text-[var(--text-primary)] rounded-xl transition-all shadow-xs group"
>
<span>Read technical writeup in {activeNode.title}</span>
<ArrowRight className="w-3.5 h-3.5 group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
)}
</div>
</div>
</div>
);
}
+60 -15
View File
@@ -5,6 +5,15 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug"; import rehypeSlug from "rehype-slug";
import rehypeHighlight from "rehype-highlight"; import rehypeHighlight from "rehype-highlight";
import {
Copy,
Check,
Info,
Lightbulb,
AlertTriangle,
Flame,
ShieldAlert,
} from "lucide-react";
function extractText(node: React.ReactNode): string { function extractText(node: React.ReactNode): string {
if (node === null || node === undefined || typeof node === "boolean") { if (node === null || node === undefined || typeof node === "boolean") {
@@ -42,16 +51,16 @@ function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>)
} }
return ( return (
<div className="code-block-wrapper relative group my-5 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-xs print:my-3 print:rounded-md print:border-slate-300 print:bg-slate-50"> <div className="code-block-wrapper relative group my-6 rounded-xl overflow-hidden border border-[var(--border-code)] bg-[var(--code-block-bg)] shadow-sm print:my-3 print:rounded-md print:border-slate-300 print:bg-slate-50">
{/* Code block header bar */} {/* Code block header bar */}
<div className="code-block-header flex items-center justify-between px-4 py-2 bg-[var(--code-header-bg)] border-b border-[var(--border-code)] text-xs font-mono select-none print:px-3 print:py-1 print:bg-slate-100 print:border-slate-300"> <div className="code-block-header flex items-center justify-between px-4 py-2.5 bg-[var(--code-header-bg)] border-b border-[var(--border-code)] text-xs font-mono select-none print:px-3 print:py-1 print:bg-slate-100 print:border-slate-300">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<div className="no-print flex items-center gap-1.5 opacity-75 group-hover:opacity-100 transition-opacity"> <div className="no-print flex items-center gap-1.5 opacity-75 group-hover:opacity-100 transition-opacity">
<span className="w-2.5 h-2.5 rounded-full bg-rose-400/80 dark:bg-rose-500/60 inline-block" /> <span className="w-2.5 h-2.5 rounded-full bg-rose-500/80 inline-block" />
<span className="w-2.5 h-2.5 rounded-full bg-amber-400/80 dark:bg-amber-500/60 inline-block" /> <span className="w-2.5 h-2.5 rounded-full bg-amber-500/80 inline-block" />
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400/80 dark:bg-emerald-500/60 inline-block" /> <span className="w-2.5 h-2.5 rounded-full bg-emerald-500/80 inline-block" />
</div> </div>
<span className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)] print:text-slate-700 group-hover:text-[var(--text-secondary)] transition-colors"> <span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-muted)] print:text-slate-700 group-hover:text-[var(--text-primary)] transition-colors">
{language} {language}
</span> </span>
</div> </div>
@@ -64,23 +73,19 @@ function PreBlock({ children, ...props }: React.HTMLAttributes<HTMLPreElement>)
> >
{copied ? ( {copied ? (
<> <>
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Check className="w-3.5 h-3.5 text-emerald-500" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-emerald-500 font-medium">Copied!</span> <span className="text-emerald-500 font-medium">Copied!</span>
</> </>
) : ( ) : (
<> <>
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Copy className="w-3.5 h-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<span>Copy</span> <span>Copy</span>
</> </>
)} )}
</button> </button>
</div> </div>
<pre className="!m-0 !bg-transparent p-4 print:p-3 overflow-x-auto text-[13.5px] print:text-[8.5pt] text-[var(--pre-text)] print:text-slate-900 font-mono leading-relaxed" {...props}> <pre className="!m-0 !bg-transparent p-4 sm:p-5 print:p-3 overflow-x-auto text-[13px] sm:text-[13.5px] print:text-[8.5pt] text-[var(--pre-text)] print:text-slate-900 font-mono leading-relaxed" {...props}>
{children} {children}
</pre> </pre>
</div> </div>
@@ -91,7 +96,7 @@ export default function Markdown({ source }: { source: string }) {
return ( return (
<div <div
className="prose max-w-none className="prose max-w-none
prose-headings:text-[var(--text-primary)] prose-headings:font-semibold prose-headings:tracking-tight prose-headings:text-[var(--text-primary)] prose-headings:font-bold prose-headings:tracking-tight
prose-h1:text-2xl sm:prose-h1:text-3xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-[var(--border-color)] prose-h1:text-2xl sm:prose-h1:text-3xl prose-h1:mb-6 prose-h1:mt-8 prose-h1:pb-3 prose-h1:border-b prose-h1:border-[var(--border-color)]
prose-h2:text-xl sm:prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-[var(--border-subtle)] prose-h2:text-xl sm:prose-h2:text-2xl prose-h2:mb-4 prose-h2:mt-8 prose-h2:pb-2 prose-h2:border-b prose-h2:border-[var(--border-subtle)]
prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6 prose-h3:text-lg prose-h3:mb-3 prose-h3:mt-6
@@ -130,8 +135,48 @@ export default function Markdown({ source }: { source: string }) {
</div> </div>
), ),
blockquote: ({ children }) => { blockquote: ({ children }) => {
const rawText = extractText(children).trim();
const alertMatch = rawText.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/i);
if (alertMatch) {
const alertType = alertMatch[1].toUpperCase();
let borderClass = "border-indigo-500 bg-indigo-500/10 text-indigo-900 dark:text-indigo-200";
let Icon = Info;
let title = "Note";
if (alertType === "TIP") {
borderClass = "border-emerald-500 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200";
Icon = Lightbulb;
title = "Tip";
} else if (alertType === "IMPORTANT") {
borderClass = "border-violet-500 bg-violet-500/10 text-violet-900 dark:text-violet-200";
Icon = Flame;
title = "Important";
} else if (alertType === "WARNING") {
borderClass = "border-amber-500 bg-amber-500/10 text-amber-900 dark:text-amber-200";
Icon = AlertTriangle;
title = "Warning";
} else if (alertType === "CAUTION") {
borderClass = "border-rose-500 bg-rose-500/10 text-rose-900 dark:text-rose-200";
Icon = ShieldAlert;
title = "Caution";
}
return ( return (
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 pl-4 py-2.5 my-5 rounded-r-lg text-[var(--text-secondary)] not-italic"> <div className={`my-5 p-4 rounded-xl border-l-4 ${borderClass} shadow-xs`}>
<div className="flex items-center gap-2 font-bold text-xs uppercase tracking-wider mb-1.5">
<Icon className="w-4 h-4" />
<span>{title}</span>
</div>
<div className="text-xs sm:text-sm text-[var(--text-secondary)] [&>p]:mb-1 [&>p:last-child]:mb-0">
{children}
</div>
</div>
);
}
return (
<blockquote className="border-l-4 border-[var(--brand)] bg-[var(--brand)]/5 dark:bg-[var(--brand)]/10 pl-4 py-2.5 my-5 rounded-r-xl text-[var(--text-secondary)] not-italic">
{children} {children}
</blockquote> </blockquote>
); );
+96 -28
View File
@@ -2,11 +2,23 @@
import { useState } from "react"; import { useState } from "react";
import Markdown from "@/components/Markdown"; import Markdown from "@/components/Markdown";
import {
Sparkles,
Terminal,
Server,
Code2,
Copy,
Check,
CheckCircle2,
Cpu,
Laptop,
} from "lucide-react";
const CONFIGS = [ const CONFIGS = [
{ {
id: "claude", id: "claude",
label: "Claude Desktop", label: "Claude Desktop",
icon: Sparkles,
lang: "json", lang: "json",
filename: "~/Library/Application Support/Claude/claude_desktop_config.json", filename: "~/Library/Application Support/Claude/claude_desktop_config.json",
content: `{ content: `{
@@ -15,7 +27,7 @@ const CONFIGS = [
"command": "bun", "command": "bun",
"args": ["run", "/path/to/mcpedia/apps/mcp/src/index.ts"], "args": ["run", "/path/to/mcpedia/apps/mcp/src/index.ts"],
"env": { "env": {
"DATABASE_URL": "postgresql://...", "DATABASE_URL": "postgresql://postgres:password@localhost:5432/mcpedia",
"OPENROUTER_API_KEY": "sk-or-v1-..." "OPENROUTER_API_KEY": "sk-or-v1-..."
} }
} }
@@ -23,10 +35,29 @@ const CONFIGS = [
}`, }`,
}, },
{ {
id: "http", id: "cursor",
label: "Remote HTTP (SSE)", label: "Cursor AI",
icon: Laptop,
lang: "json", lang: "json",
filename: "Streamable HTTP Endpoint", filename: ".cursor/mcp.json",
content: `{
"mcpServers": {
"mcpedia": {
"command": "bun",
"args": ["run", "/path/to/mcpedia/apps/mcp/src/index.ts"],
"env": {
"DATABASE_URL": "postgresql://postgres:password@localhost:5432/mcpedia"
}
}
}
}`,
},
{
id: "http",
label: "Streamable HTTP Endpoint",
icon: Server,
lang: "json",
filename: "Streamable HTTP (SSE) Client Config",
content: `{ content: `{
"mcpServers": { "mcpServers": {
"mcpedia-remote": { "mcpedia-remote": {
@@ -40,19 +71,21 @@ const CONFIGS = [
}, },
{ {
id: "cli", id: "cli",
label: "CLI Client", label: "CLI & Scripts",
icon: Terminal,
lang: "bash", lang: "bash",
filename: "Terminal One-Shot / REPL", filename: "Terminal / MCP Client Subprocess",
content: `# One-shot question content: `# Ask questions over semantic knowledge
bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work?" bun run apps/mcp-client/src/ask.ts "How does ret2win stack alignment work in x86_64?"
# Interactive REPL # Run interactive REPL with native tool calls
bun run apps/mcp-client/src/index.ts`, bun run apps/mcp-client/src/index.ts`,
}, },
]; ];
export default function McpConfigSnippet() { export default function McpConfigSnippet() {
const [activeTab, setActiveTab] = useState("claude"); const [activeTab, setActiveTab] = useState("claude");
const [copied, setCopied] = useState(false);
const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0]; const current = CONFIGS.find((c) => c.id === activeTab) || CONFIGS[0];
@@ -60,45 +93,80 @@ export default function McpConfigSnippet() {
${current.content} ${current.content}
\`\`\``; \`\`\``;
function handleCopyAll() {
navigator.clipboard.writeText(current.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return ( return (
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl overflow-hidden shadow-sm"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl overflow-hidden shadow-sm">
{/* Header bar with tabs */} {/* Header bar with tabs */}
<div className="flex items-center justify-between px-4 py-2.5 bg-[var(--bg-elevated)] border-b border-[var(--border-color)] flex-wrap gap-2"> <div className="flex items-center justify-between px-4 sm:px-6 py-3 bg-[var(--bg-elevated)]/60 border-b border-[var(--border-color)] flex-wrap gap-2">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5 flex-wrap">
{CONFIGS.map((c) => ( {CONFIGS.map((c) => {
const Icon = c.icon;
const isSelected = activeTab === c.id;
return (
<button <button
key={c.id} key={c.id}
onClick={() => setActiveTab(c.id)} onClick={() => setActiveTab(c.id)}
className={`px-3 py-1.5 text-xs rounded-lg font-medium transition-all cursor-pointer ${ type="button"
activeTab === c.id className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-xl font-semibold transition-all cursor-pointer ${
? "bg-[var(--brand)] text-white shadow-xs" isSelected
? "bg-[var(--brand)] text-white shadow-md shadow-[var(--brand)]/20"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-surface)]"
}`} }`}
> >
{c.label} <Icon className="w-3.5 h-3.5" />
<span>{c.label}</span>
</button> </button>
))} );
})}
</div> </div>
<div className="text-[11px] text-[var(--text-dim)] font-mono hidden sm:block"> <button
{current.filename} onClick={handleCopyAll}
</div> type="button"
className="flex items-center gap-1.5 px-3 py-1 text-xs bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer font-medium"
>
{copied ? (
<>
<Check className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-500">Copied Config</span>
</>
) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copy Config</span>
</>
)}
</button>
</div> </div>
{/* Code preview via Markdown syntax highlighter */} {/* Code preview via Markdown syntax highlighter */}
<div className="p-4 [&>.prose>.group]:my-0 [&>.prose]:max-w-none"> <div className="p-4 sm:p-5 [&>.prose>.group]:my-0 [&>.prose]:max-w-none">
<Markdown source={markdownSnippet} /> <Markdown source={markdownSnippet} />
</div> </div>
<div className="px-4 py-2 bg-[var(--bg-elevated)] border-t border-[var(--border-color)] text-[11px] text-[var(--text-dim)] flex items-center justify-between flex-wrap gap-2"> {/* Footer Info */}
<span> <div className="px-4 sm:px-6 py-3 bg-[var(--bg-elevated)]/40 border-t border-[var(--border-color)] text-xs text-[var(--text-dim)] flex flex-col sm:flex-row sm:items-center justify-between gap-2">
Config target: <span className="font-mono text-[var(--text-secondary)]">{current.filename}</span> <div className="flex items-center gap-2">
<span className="font-mono uppercase text-[10px] text-[var(--text-muted)] font-semibold">
Target File:
</span> </span>
<span className="text-[10px] text-[var(--text-muted)]"> <span className="font-mono text-[var(--text-secondary)] text-[11px]">
Format: <span className="uppercase font-mono font-semibold">{current.lang}</span> {current.filename}
</span> </span>
</div> </div>
<div className="flex items-center gap-3 text-[11px]">
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400 font-medium">
<CheckCircle2 className="w-3.5 h-3.5" />
<span>13 MCP Tools Ready</span>
</span>
</div>
</div>
</div> </div>
); );
} }
+49 -48
View File
@@ -14,6 +14,16 @@ import {
type ExportSortOption, type ExportSortOption,
} from "@mcpedia/core/export-helpers"; } from "@mcpedia/core/export-helpers";
import { getSectionMeta } from "@mcpedia/config/sections"; import { getSectionMeta } from "@mcpedia/config/sections";
import {
Printer,
FileDown,
Copy,
Check,
ArrowLeft,
Filter,
ListOrdered,
BookOpen,
} from "lucide-react";
interface PdfExportViewProps { interface PdfExportViewProps {
exportData: ExportData; exportData: ExportData;
@@ -113,23 +123,21 @@ export default function PdfExportView({
return ( return (
<div className="pb-16 print:pb-0"> <div className="pb-16 print:pb-0">
{/* ========================================================================= {/* Export Control Toolbar (Screen Only — Hidden in Print) */}
Export Control Toolbar (Screen Only — Hidden in Print) <aside className="no-print export-toolbar sticky top-16 z-30 mb-8 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-5 shadow-md">
========================================================================= */}
<aside className="no-print export-toolbar sticky top-16 z-30 mb-8 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 shadow-sm">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4"> <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
{/* Left info & Back button */} {/* Left info & Back button */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3.5">
<Link <Link
href={backHref} href={backHref}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all shadow-xs" className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all shadow-xs"
> >
<span>←</span> <ArrowLeft className="w-3.5 h-3.5" />
<span>Back</span> <span>Back</span>
</Link> </Link>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-mono text-[var(--text-primary)] font-semibold uppercase"> <span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] font-bold uppercase">
{sectionMeta.label} {sectionMeta.label}
</span> </span>
<span className="text-xs text-[var(--text-dim)]">·</span> <span className="text-xs text-[var(--text-dim)]">·</span>
@@ -150,13 +158,19 @@ export default function PdfExportView({
<button <button
onClick={handleCopyMarkdown} onClick={handleCopyMarkdown}
type="button" type="button"
className="inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-xs cursor-pointer" className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-xl transition-all shadow-xs cursor-pointer"
title="Copy compiled markdown document" title="Copy compiled markdown document"
> >
{copiedMd ? ( {copiedMd ? (
<span className="text-emerald-600 font-bold">✓ Copied</span> <>
<Check className="w-3.5 h-3.5 text-emerald-500" />
<span className="text-emerald-500 font-bold">Copied</span>
</>
) : ( ) : (
<>
<Copy className="w-3.5 h-3.5" />
<span>Copy Markdown</span> <span>Copy Markdown</span>
</>
)} )}
</button> </button>
@@ -164,9 +178,10 @@ export default function PdfExportView({
<button <button
onClick={handleDownloadMarkdown} onClick={handleDownloadMarkdown}
type="button" type="button"
className="inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-md transition-all shadow-xs cursor-pointer" className="inline-flex items-center gap-1.5 px-3.5 py-2 text-xs font-semibold bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-xl transition-all shadow-xs cursor-pointer"
title="Download compiled .md file" title="Download compiled .md file"
> >
<FileDown className="w-3.5 h-3.5" />
<span>Download .md</span> <span>Download .md</span>
</button> </button>
@@ -174,22 +189,10 @@ export default function PdfExportView({
<button <button
onClick={handlePrint} onClick={handlePrint}
type="button" type="button"
className="inline-flex items-center gap-2 px-4 py-2 text-xs font-semibold bg-slate-900 hover:bg-slate-800 text-white rounded-md shadow-sm transition-all cursor-pointer hover:shadow" className="inline-flex items-center gap-2 px-4 py-2 text-xs font-bold bg-slate-900 hover:bg-slate-800 dark:bg-white dark:hover:bg-slate-100 text-white dark:text-slate-900 rounded-xl shadow-md transition-all cursor-pointer hover:shadow-lg"
title="Open browser print dialog to save as PDF" title="Open browser print dialog to save as PDF"
> >
<svg <Printer className="w-4 h-4" />
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"
/>
</svg>
<span>Print / Save as PDF</span> <span>Print / Save as PDF</span>
</button> </button>
</div> </div>
@@ -199,11 +202,11 @@ export default function PdfExportView({
<div className="mt-4 pt-3.5 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs"> <div className="mt-4 pt-3.5 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
{/* Sort Order Selector */} {/* Sort Order Selector */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[var(--text-muted)] font-medium">Sort by:</span> <span className="text-[var(--text-muted)] font-medium font-mono text-[11px] uppercase">Sort by:</span>
<select <select
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value as ExportSortOption)} onChange={(e) => setSortBy(e.target.value as ExportSortOption)}
className="bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md px-2.5 py-1 text-xs text-[var(--text-primary)] focus:outline-none focus:border-slate-500" className="bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg px-3 py-1 text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--brand)]"
> >
<option value="category_points">Category & Points (Standard)</option> <option value="category_points">Category & Points (Standard)</option>
<option value="points_desc">Points (Highest First)</option> <option value="points_desc">Points (Highest First)</option>
@@ -217,23 +220,23 @@ export default function PdfExportView({
{/* Section toggles */} {/* Section toggles */}
<div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]"> <div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]">
{cleanDocs.length > 1 && ( {cleanDocs.length > 1 && (
<label className="inline-flex items-center gap-1.5 cursor-pointer"> <label className="inline-flex items-center gap-1.5 cursor-pointer select-none">
<input <input
type="checkbox" type="checkbox"
checked={showTOC} checked={showTOC}
onChange={(e) => setShowTOC(e.target.checked)} onChange={(e) => setShowTOC(e.target.checked)}
className="rounded border-[var(--border-color)] text-slate-800" className="rounded border-[var(--border-color)] text-indigo-600"
/> />
<span>Table of Contents</span> <span>Table of Contents</span>
</label> </label>
)} )}
<label className="inline-flex items-center gap-1.5 cursor-pointer"> <label className="inline-flex items-center gap-1.5 cursor-pointer select-none">
<input <input
type="checkbox" type="checkbox"
checked={pageBreaks} checked={pageBreaks}
onChange={(e) => setPageBreaks(e.target.checked)} onChange={(e) => setPageBreaks(e.target.checked)}
className="rounded border-[var(--border-color)] text-slate-800" className="rounded border-[var(--border-color)] text-indigo-600"
/> />
<span>Page Break per Challenge</span> <span>Page Break per Challenge</span>
</label> </label>
@@ -243,13 +246,13 @@ export default function PdfExportView({
{/* Category filter pills */} {/* Category filter pills */}
{allCategories.length > 1 && ( {allCategories.length > 1 && (
<div className="mt-3 pt-3 border-t border-[var(--border-color)] flex items-center gap-1.5 flex-wrap text-xs"> <div className="mt-3 pt-3 border-t border-[var(--border-color)] flex items-center gap-1.5 flex-wrap text-xs">
<span className="text-[var(--text-muted)] mr-1">Filter:</span> <span className="text-[var(--text-muted)] font-mono text-[11px] uppercase mr-1">Filter:</span>
<button <button
onClick={() => setSelectedCategory("all")} onClick={() => setSelectedCategory("all")}
type="button" type="button"
className={`px-2.5 py-0.5 rounded-md text-xs font-mono transition-all cursor-pointer ${ className={`px-3 py-1 rounded-lg text-xs font-mono transition-all cursor-pointer ${
selectedCategory === "all" selectedCategory === "all"
? "bg-slate-900 text-white font-semibold" ? "bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold"
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
}`} }`}
> >
@@ -264,9 +267,9 @@ export default function PdfExportView({
key={cat} key={cat}
onClick={() => setSelectedCategory(cat)} onClick={() => setSelectedCategory(cat)}
type="button" type="button"
className={`px-2.5 py-0.5 rounded-md text-xs font-mono transition-all cursor-pointer ${ className={`px-3 py-1 rounded-lg text-xs font-mono transition-all cursor-pointer ${
selectedCategory.toLowerCase() === cat.toLowerCase() selectedCategory.toLowerCase() === cat.toLowerCase()
? "bg-slate-900 text-white font-semibold" ? "bg-slate-900 dark:bg-white text-white dark:text-slate-900 font-bold"
: "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]" : "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]"
}`} }`}
> >
@@ -278,14 +281,11 @@ export default function PdfExportView({
)} )}
</aside> </aside>
{/* ========================================================================= {/* Writeup Document Flow */}
Clean, Simple, Human-Written Writeup Document Flow (No Cards) <main className="print-container bg-white text-slate-900 max-w-4xl mx-auto px-4 sm:px-8 py-6 print:p-0 print:max-w-none rounded-2xl border border-[var(--border-color)] print:border-none shadow-sm print:shadow-none">
========================================================================= */}
<main className="print-container bg-white text-slate-900 max-w-4xl mx-auto px-4 sm:px-8 py-6 print:p-0 print:max-w-none">
{/* Document Header */} {/* Document Header */}
<header className="mb-8 pb-3 border-b border-slate-300"> <header className="mb-8 pb-4 border-b border-slate-300">
<h1 className="text-3xl sm:text-4xl font-bold text-slate-950 tracking-tight mb-2"> <h1 className="text-3xl sm:text-4xl font-extrabold text-slate-950 tracking-tight mb-2">
{summary.title} {summary.title}
</h1> </h1>
<div className="text-xs text-slate-500 font-mono flex flex-wrap items-center gap-x-2 gap-y-1"> <div className="text-xs text-slate-500 font-mono flex flex-wrap items-center gap-x-2 gap-y-1">
@@ -297,11 +297,12 @@ export default function PdfExportView({
</div> </div>
</header> </header>
{/* Table of Contents (Simple & Clean) */} {/* Table of Contents */}
{showTOC && filteredAndSortedDocs.length > 1 && ( {showTOC && filteredAndSortedDocs.length > 1 && (
<section className="toc-section mb-10 pb-6 border-b border-slate-200"> <section className="toc-section mb-10 pb-6 border-b border-slate-200">
<h2 className="text-sm font-bold uppercase font-mono text-slate-700 mb-3 tracking-wider"> <h2 className="text-sm font-bold uppercase font-mono text-slate-700 mb-3 tracking-wider flex items-center gap-1.5">
Table of Contents <BookOpen className="w-4 h-4" />
<span>Table of Contents</span>
</h2> </h2>
<ol className="list-decimal list-inside space-y-1.5 text-xs text-slate-800"> <ol className="list-decimal list-inside space-y-1.5 text-xs text-slate-800">
{filteredAndSortedDocs.map((doc, idx) => { {filteredAndSortedDocs.map((doc, idx) => {
@@ -326,7 +327,7 @@ export default function PdfExportView({
</section> </section>
)} )}
{/* Challenges / Writeups Flow ("Mirip Writeup Manusia") */} {/* Challenges / Writeups Flow */}
<div className="challenges-flow space-y-10 print:space-y-0"> <div className="challenges-flow space-y-10 print:space-y-0">
{filteredAndSortedDocs.map((doc, idx) => { {filteredAndSortedDocs.map((doc, idx) => {
const chNum = idx + 1; const chNum = idx + 1;
@@ -353,7 +354,7 @@ export default function PdfExportView({
{pts > 0 && <span><strong>Points:</strong> {pts} pts</span>} {pts > 0 && <span><strong>Points:</strong> {pts} pts</span>}
{diff && <span><strong>Difficulty:</strong> {diff}</span>} {diff && <span><strong>Difficulty:</strong> {diff}</span>}
{/* Extra fields (excluding author, slug, scope, etc.) */} {/* Extra fields */}
{Object.entries(extra).map(([k, v]) => { {Object.entries(extra).map(([k, v]) => {
const lowerKey = k.toLowerCase(); const lowerKey = k.toLowerCase();
if ( if (
+53 -39
View File
@@ -4,6 +4,16 @@ import { useEffect, useState, useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { getSectionMeta } from "@mcpedia/config/sections"; import { getSectionMeta } from "@mcpedia/config/sections";
import {
Folder,
FolderOpen,
FileText,
ChevronRight,
Search,
X,
Plus,
Compass,
} from "lucide-react";
interface Doc { interface Doc {
slug: string; slug: string;
@@ -102,11 +112,11 @@ function TreeNodeItem({
const isCollapsed = collapsedMap[node.slug] ?? false; const isCollapsed = collapsedMap[node.slug] ?? false;
return ( return (
<li className={`select-none ${depth > 0 ? "ml-3 pl-2 border-l border-[var(--border-color)]" : ""}`}> <li className={`select-none ${depth > 0 ? "ml-3.5 pl-2 border-l border-[var(--border-color)]" : ""}`}>
<div <div
className={`group flex items-center justify-between gap-1.5 text-xs py-1.5 px-2 rounded-md transition-all ${ className={`group flex items-center justify-between gap-1 text-xs py-1.5 px-2 rounded-lg transition-all ${
isActive isActive
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-sm" ? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-semibold border border-[var(--brand)]/30 shadow-xs"
: isParentOfActive : isParentOfActive
? "text-[var(--text-primary)] bg-[var(--bg-elevated)]" ? "text-[var(--text-primary)] bg-[var(--bg-elevated)]"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
@@ -117,15 +127,23 @@ function TreeNodeItem({
className="flex items-center gap-2 min-w-0 flex-1 py-0.5" className="flex items-center gap-2 min-w-0 flex-1 py-0.5"
title={node.title || node.name} title={node.title || node.name}
> >
<span className="text-xs shrink-0 opacity-80 group-hover:opacity-100"> <span className="shrink-0 text-[var(--text-dim)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors">
{isFolder ? "📁" : "📄"} {isFolder ? (
isCollapsed ? (
<Folder className="w-3.5 h-3.5" />
) : (
<FolderOpen className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
)
) : (
<FileText className="w-3.5 h-3.5" />
)}
</span> </span>
<span className="truncate">{node.title || node.name}</span> <span className="truncate">{node.title || node.name}</span>
</Link> </Link>
{isFolder && ( {isFolder && (
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
<span className="text-[10px] text-[var(--text-muted)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-1 py-0.2 rounded font-mono"> <span className="text-[10px] text-[var(--text-dim)] bg-[var(--bg-surface)] border border-[var(--border-color)] px-1.5 py-0.2 rounded font-mono">
{node.docCount} {node.docCount}
</span> </span>
<button <button
@@ -135,24 +153,19 @@ function TreeNodeItem({
e.stopPropagation(); e.stopPropagation();
toggleCollapse(node.slug); toggleCollapse(node.slug);
}} }}
className="p-0.5 text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated-hover)] transition-transform" className="p-1 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated-hover)] transition-transform"
aria-label={isCollapsed ? "Expand folder" : "Collapse folder"} aria-label={isCollapsed ? "Expand folder" : "Collapse folder"}
> >
<svg <ChevronRight
className={`w-3 h-3 transition-transform duration-200 ${isCollapsed ? "-rotate-90" : "rotate-0"}`} className={`w-3 h-3 transition-transform duration-200 ${isCollapsed ? "rotate-0" : "rotate-90"}`}
fill="none" />
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button> </button>
</div> </div>
)} )}
</div> </div>
{isFolder && !isCollapsed && ( {isFolder && !isCollapsed && (
<ul className="mt-0.5 space-y-0.5 animate-fade-in"> <ul className="mt-1 space-y-0.5 animate-fade-in">
{node.children.map((child) => ( {node.children.map((child) => (
<TreeNodeItem <TreeNodeItem
key={child.slug} key={child.slug}
@@ -212,7 +225,7 @@ export default function Sidebar() {
}, [filteredDocs]); }, [filteredDocs]);
return ( return (
<nav className="h-full flex flex-col py-4 px-3"> <nav className="h-full flex flex-col py-4 px-3 select-none">
{/* Sidebar search filter */} {/* Sidebar search filter */}
<div className="mb-3"> <div className="mb-3">
<div className="relative"> <div className="relative">
@@ -220,31 +233,25 @@ export default function Sidebar() {
type="text" type="text"
value={filterText} value={filterText}
onChange={(e) => setFilterText(e.target.value)} onChange={(e) => setFilterText(e.target.value)}
placeholder="Filter docs..." placeholder="Filter knowledge tree..."
className="w-full pl-7 pr-3 py-1.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-md text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] transition-colors" className="w-full pl-8 pr-7 py-1.5 bg-[var(--bg-surface)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-xs text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[var(--brand)] transition-all"
/> />
<svg <Search className="w-3.5 h-3.5 text-[var(--text-dim)] absolute left-2.5 top-2.5 pointer-events-none" />
className="w-3.5 h-3.5 text-[var(--text-muted)] absolute left-2 top-2.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{filterText && ( {filterText && (
<button <button
onClick={() => setFilterText("")} onClick={() => setFilterText("")}
className="absolute right-2 top-2 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)]" className="absolute right-2 top-2 p-0.5 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded hover:bg-[var(--bg-elevated)]"
aria-label="Clear filter"
> >
✕ <X className="w-3.5 h-3.5" />
</button> </button>
)} )}
</div> </div>
</div> </div>
{/* Summary count */} {/* Summary count */}
<div className="flex items-center justify-between px-1 mb-3 text-[11px] text-[var(--text-dim)]"> <div className="flex items-center justify-between px-1 mb-2 text-[10px] text-[var(--text-dim)] font-mono uppercase tracking-wider">
<span className="uppercase tracking-wider font-mono">Knowledge Base</span> <span>Knowledge Index</span>
<span>{filteredDocs.length} items</span> <span>{filteredDocs.length} items</span>
</div> </div>
@@ -261,17 +268,17 @@ export default function Sidebar() {
<div key={id} className="border-b border-[var(--border-color)] pb-3 last:border-0"> <div key={id} className="border-b border-[var(--border-color)] pb-3 last:border-0">
<Link <Link
href={`/${id}`} href={`/${id}`}
className={`flex items-center justify-between gap-1.5 px-1 py-1 mb-1 rounded text-xs font-semibold uppercase tracking-wider transition-colors ${ className={`flex items-center justify-between gap-1.5 px-2 py-1.5 mb-1 rounded-lg text-xs font-semibold uppercase tracking-wider transition-colors ${
isSectionActive isSectionActive
? "text-[var(--brand)] dark:text-[var(--accent)]" ? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`} }`}
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-2">
<span>{icon}</span> <span className="text-xs">{icon}</span>
<span>{label}</span> <span>{label}</span>
</div> </div>
<span className="text-[10px] text-[var(--text-dim)] font-normal font-mono"> <span className="text-[10px] text-[var(--text-dim)] font-normal font-mono bg-[var(--bg-surface)] px-1.5 py-0.5 rounded border border-[var(--border-color)]">
{sectionDocs.length} {sectionDocs.length}
</span> </span>
</Link> </Link>
@@ -291,15 +298,22 @@ export default function Sidebar() {
</div> </div>
); );
})} })}
{filteredDocs.length === 0 && (
<div className="text-center py-8 text-xs text-[var(--text-dim)]">
No matching documents
</div>
)}
</div> </div>
{/* Bottom quick links */} {/* Bottom quick links */}
<div className="pt-3 border-t border-[var(--border-color)] mt-auto"> <div className="pt-3 border-t border-[var(--border-color)] mt-auto">
<Link <Link
href="/create" href="/create"
className="flex items-center justify-center gap-1.5 w-full py-1.5 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-md transition-colors font-medium" className="flex items-center justify-center gap-2 w-full py-2 text-xs bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg transition-all font-medium shadow-xs group"
> >
<span>+ Create New Document</span> <Plus className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)] group-hover:scale-110 transition-transform" />
<span>Create New Document</span>
</Link> </Link>
</div> </div>
</nav> </nav>
+22 -10
View File
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import GithubSlugger from "github-slugger"; import GithubSlugger from "github-slugger";
import { AlignLeft, ArrowUp, Hash } from "lucide-react";
interface TOCEntry { interface TOCEntry {
id: string; id: string;
@@ -58,18 +59,29 @@ export default function TOC({ source }: { source: string }) {
if (toc.length === 0) return null; if (toc.length === 0) return null;
function scrollToTop() {
window.scrollTo({ top: 0, behavior: "smooth" });
}
return ( return (
<nav className="text-xs"> <nav className="text-xs space-y-4">
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-[var(--border-color)]"> <div className="flex items-center justify-between pb-2 border-b border-[var(--border-color)]">
<svg className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <div className="flex items-center gap-1.5 font-semibold uppercase tracking-wider text-[var(--text-muted)] text-[11px] font-mono">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" /> <AlignLeft className="w-3.5 h-3.5 text-[var(--brand)] dark:text-[var(--accent)]" />
</svg> <span>On This Page</span>
<span className="font-semibold uppercase tracking-wider text-[var(--text-muted)] text-[11px]"> </div>
On this page <button
</span> onClick={scrollToTop}
type="button"
className="text-[10px] text-[var(--text-dim)] hover:text-[var(--text-primary)] flex items-center gap-0.5 hover:underline"
title="Scroll to top"
>
<ArrowUp className="w-3 h-3" />
<span>Top</span>
</button>
</div> </div>
<ul className="space-y-1.5 border-l border-[var(--border-color)] pl-2.5"> <ul className="space-y-1.5 border-l border-[var(--border-color)] pl-3">
{toc.map((entry) => { {toc.map((entry) => {
const isActive = activeId === entry.id; const isActive = activeId === entry.id;
return ( return (
@@ -90,7 +102,7 @@ export default function TOC({ source }: { source: string }) {
history.pushState(null, "", `#${entry.id}`); history.pushState(null, "", `#${entry.id}`);
} }
}} }}
className={`block py-0.5 transition-colors line-clamp-1 ${ className={`block py-0.5 transition-all line-clamp-1 ${
isActive isActive
? "text-[var(--brand)] dark:text-[var(--accent)] font-semibold translate-x-0.5" ? "text-[var(--brand)] dark:text-[var(--accent)] font-semibold translate-x-0.5"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)]" : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
+5 -8
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Sun, Moon } from "lucide-react";
const STORAGE_KEY = "mcpedia-theme"; const STORAGE_KEY = "mcpedia-theme";
@@ -36,7 +37,7 @@ export default function ThemeToggle() {
if (!mounted) { if (!mounted) {
return ( return (
<div className="w-8 h-8 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] opacity-50" /> <div className="w-8 h-8 rounded-lg bg-[var(--bg-elevated)] border border-[var(--border-color)] opacity-40" />
); );
} }
@@ -46,16 +47,12 @@ export default function ThemeToggle() {
type="button" type="button"
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"} aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
title={isDark ? "Switch to light mode" : "Switch to dark mode"} title={isDark ? "Switch to light mode" : "Switch to dark mode"}
className="p-1.5 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)] border border-transparent hover:border-[var(--border-color)] transition-all flex items-center justify-center" className="p-2 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)] border border-transparent hover:border-[var(--border-color)] transition-all flex items-center justify-center cursor-pointer"
> >
{isDark ? ( {isDark ? (
<svg className="w-4 h-4 text-amber-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Sun className="w-4 h-4 text-amber-400 hover:rotate-45 transition-transform" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : ( ) : (
<svg className="w-4 h-4 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Moon className="w-4 h-4 text-indigo-600 hover:-rotate-12 transition-transform" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
)} )}
</button> </button>
); );
+9 -6
View File
@@ -4,6 +4,7 @@ import { WEBHOOK_SECRET } from "@mcpedia/config";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import DocForm from "@/components/DocForm"; import DocForm from "@/components/DocForm";
import { Edit3, ArrowLeft, Plus } from "lucide-react";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -33,14 +34,16 @@ export default async function CreatePage() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between pb-4 border-b border-[var(--border-color)]"> <div className="flex items-center justify-between pb-4 border-b border-[var(--border-color)]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3.5">
<div className="w-10 h-10 rounded-lg bg-[var(--brand)]/15 border border-[var(--brand)]/30 flex items-center justify-center text-xl text-[var(--brand)] dark:text-[var(--accent)]"> <div className="w-11 h-11 rounded-2xl bg-[var(--brand)]/15 border border-[var(--brand)]/30 flex items-center justify-center text-[var(--brand)] dark:text-[var(--accent)]">
✏️ <Plus className="w-6 h-6" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Create Document</h1> <h1 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
<p className="text-xs text-[var(--text-muted)] mt-0.5"> Create Knowledge Document
Writes directly to PostgreSQL, records revision history, and computes vector embeddings. </h1>
<p className="text-xs text-[var(--text-muted)] mt-0.5 font-mono">
Persists directly into PostgreSQL, tracks revision history, and generates vector embeddings.
</p> </p>
</div> </div>
</div> </div>
+11 -7
View File
@@ -1,8 +1,9 @@
import React from "react"; import React from "next/link";
import Link from "next/link"; import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getExportDocuments, formatScopeTitle, type ExportSortOption } from "@mcpedia/core"; import { getExportDocuments, formatScopeTitle, type ExportSortOption } from "@mcpedia/core";
import PdfExportView from "@/components/PdfExportView"; import PdfExportView from "@/components/PdfExportView";
import { FileText, ArrowLeft } from "lucide-react";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -43,14 +44,16 @@ export default async function ExportPdfPage({
if (exportData.documents.length === 0) { if (exportData.documents.length === 0) {
return ( return (
<div className="py-16 text-center space-y-4 max-w-lg mx-auto"> <div className="py-16 text-center space-y-4 max-w-lg mx-auto bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-3xl p-8 shadow-sm">
<div className="text-4xl">📄</div> <div className="w-14 h-14 rounded-2xl bg-[var(--bg-elevated)] text-[var(--text-dim)] flex items-center justify-center mx-auto">
<FileText className="w-7 h-7" />
</div>
<h1 className="text-2xl font-bold text-[var(--text-primary)]"> <h1 className="text-2xl font-bold text-[var(--text-primary)]">
No Documents Found for Export No Documents Found for Export
</h1> </h1>
<p className="text-sm text-[var(--text-muted)]"> <p className="text-xs sm:text-sm text-[var(--text-muted)] leading-relaxed">
Could not find any published documents matching the path{" "} Could not find any published documents matching the path{" "}
<code className="text-[var(--brand)] font-mono"> <code className="text-[var(--brand)] dark:text-[var(--accent)] font-mono font-semibold">
{targetPath || section || "root"} {targetPath || section || "root"}
</code> </code>
. .
@@ -58,9 +61,10 @@ export default async function ExportPdfPage({
<div className="pt-4"> <div className="pt-4">
<Link <Link
href={backHref} href={backHref}
className="inline-flex items-center gap-1.5 px-4 py-2 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white text-xs font-semibold rounded-lg shadow-sm transition-all" className="inline-flex items-center gap-1.5 px-4 py-2 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white text-xs font-semibold rounded-xl shadow-md shadow-[var(--brand)]/20 transition-all"
> >
<span>← Return to Knowledge Base</span> <ArrowLeft className="w-3.5 h-3.5" />
<span>Return to Knowledge Base</span>
</Link> </Link>
</div> </div>
</div> </div>
+158 -154
View File
@@ -20,13 +20,15 @@
--brand: #4f46e5; --brand: #4f46e5;
--brand-hover: #4338ca; --brand-hover: #4338ca;
--brand-glow: rgba(79, 70, 229, 0.15);
--accent: #4f46e5; --accent: #4f46e5;
--accent-hover: #4338ca; --accent-hover: #4338ca;
--cyan-accent: #0284c7;
--nav-bg: rgba(255, 255, 255, 0.88); --nav-bg: rgba(255, 255, 255, 0.85);
/* Code & Syntax variables (Light Mode) */ /* Code & Syntax variables (Light Mode) */
--code-bg: #f1f5f9; --code-bg: #f8fafc;
--code-block-bg: #f8fafc; --code-block-bg: #f8fafc;
--code-header-bg: #f1f5f9; --code-header-bg: #f1f5f9;
--border-code: #e2e8f0; --border-code: #e2e8f0;
@@ -53,47 +55,52 @@
--hl-operator: #57606a; --hl-operator: #57606a;
--hl-variable: #953800; --hl-variable: #953800;
--hl-meta: #6e7781; --hl-meta: #6e7781;
/* Grid overlay */
--grid-pattern: rgba(15, 23, 42, 0.03);
} }
/* Dark mode overrides (html.dark) */ /* Dark mode overrides (html.dark) */
html.dark { html.dark {
color-scheme: dark; color-scheme: dark;
--bg-app: #08090a; --bg-app: #08090d;
--bg-surface: #0f1011; --bg-surface: #0e1117;
--bg-surface-hover: #131415; --bg-surface-hover: #141822;
--bg-elevated: #141517; --bg-elevated: #151a26;
--bg-elevated-hover: #1b1d20; --bg-elevated-hover: #1e2436;
--border-color: #1f2022; --border-color: #1e2536;
--border-subtle: rgba(255, 255, 255, 0.06); --border-subtle: rgba(255, 255, 255, 0.06);
--border-card: #1f2022; --border-card: #1e2536;
--text-primary: #f7f8f8; --text-primary: #f8fafc;
--text-secondary: #d0d6e0; --text-secondary: #cbd5e1;
--text-muted: #8a8f98; --text-muted: #8492a6;
--text-dim: #62666d; --text-dim: #505c70;
--brand: #5e6ad2; --brand: #6366f1;
--brand-hover: #7170ff; --brand-hover: #818cf8;
--accent: #7170ff; --brand-glow: rgba(99, 102, 241, 0.22);
--accent-hover: #828fff; --accent: #818cf8;
--accent-hover: #a5b4fc;
--cyan-accent: #38bdf8;
--nav-bg: rgba(8, 9, 10, 0.85); --nav-bg: rgba(8, 9, 13, 0.85);
/* Code & Syntax variables (Dark Mode) */ /* Code & Syntax variables (Dark Mode) */
--code-bg: #141517; --code-bg: #0b0e14;
--code-block-bg: #0d1117; --code-block-bg: #0a0d14;
--code-header-bg: #161b22; --code-header-bg: #111520;
--border-code: #30363d; --border-code: #202738;
--code-button-bg: #21262d; --code-button-bg: #151a26;
--code-button-hover-bg: #30363d; --code-button-hover-bg: #202738;
--pre-text: #e6edf3; --pre-text: #e2e8f0;
--code-inline-bg: #161b22; --code-inline-bg: #131722;
--code-inline-text: #e6edf3; --code-inline-text: #e2e8f0;
--code-inline-border: #30363d; --code-inline-border: #202738;
--table-zebra: rgba(255, 255, 255, 0.02); --table-zebra: rgba(255, 255, 255, 0.02);
--scrollbar-thumb: #23252a; --scrollbar-thumb: #1e2536;
--scrollbar-thumb-hover: #383b42; --scrollbar-thumb-hover: #313c54;
--shadow-card: none; --shadow-card: none;
/* Syntax Highlighting (Dark Mode) */ /* Syntax Highlighting (Dark Mode) */
@@ -107,6 +114,9 @@ html.dark {
--hl-operator: #8b949e; --hl-operator: #8b949e;
--hl-variable: #ffa657; --hl-variable: #ffa657;
--hl-meta: #8b949e; --hl-meta: #8b949e;
/* Grid overlay */
--grid-pattern: rgba(255, 255, 255, 0.025);
} }
@theme inline { @theme inline {
@@ -141,10 +151,19 @@ body {
background-color: var(--bg-app); background-color: var(--bg-app);
color: var(--text-secondary); color: var(--text-secondary);
font-family: var(--font-sans); font-family: var(--font-sans);
font-feature-settings: "cv01", "ss03"; font-feature-settings: "cv01", "ss03", "zero";
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
transition: background-color 0.2s ease, color 0.2s ease; transition: background-color 0.25s ease, color 0.25s ease;
position: relative;
min-height: 100vh;
}
/* Background subtle tech grid */
.tech-grid-bg {
background-size: 32px 32px;
background-image: linear-gradient(to right, var(--grid-pattern) 1px, transparent 1px),
linear-gradient(to bottom, var(--grid-pattern) 1px, transparent 1px);
} }
/* Custom scrollbar */ /* Custom scrollbar */
@@ -169,44 +188,100 @@ body {
/* Glassmorphism helpers */ /* Glassmorphism helpers */
.glass-panel { .glass-panel {
background: var(--bg-surface); background: var(--bg-surface);
backdrop-filter: blur(12px); backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
} }
.glass-nav { .glass-nav {
background: var(--nav-bg); background: var(--nav-bg);
backdrop-filter: blur(16px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(20px);
}
.glow-card {
position: relative;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.glow-card:hover {
transform: translateY(-2px);
border-color: var(--brand);
box-shadow: 0 8px 30px -10px var(--brand-glow);
}
/* Callout Alert Styles */
.callout {
border-left-width: 4px;
border-radius: 0 0.5rem 0.5rem 0;
padding: 1rem 1.25rem;
margin: 1.5rem 0;
}
.callout-note {
border-left-color: #6366f1;
background-color: rgba(99, 102, 241, 0.08);
}
.callout-tip {
border-left-color: #10b981;
background-color: rgba(16, 185, 129, 0.08);
}
.callout-important {
border-left-color: #8b5cf6;
background-color: rgba(139, 92, 246, 0.08);
}
.callout-warning {
border-left-color: #f59e0b;
background-color: rgba(245, 158, 11, 0.08);
}
.callout-caution {
border-left-color: #ef4444;
background-color: rgba(239, 68, 68, 0.08);
} }
/* Prose markdown typography for both Light and Dark modes */ /* Prose markdown typography for both Light and Dark modes */
.prose { .prose {
color: var(--text-secondary) !important; color: var(--text-secondary) !important;
max-width: 100% !important;
} }
.prose h1, .prose h2, .prose h3, .prose h4, .prose strong { .prose h1, .prose h2, .prose h3, .prose h4, .prose strong {
color: var(--text-primary) !important; color: var(--text-primary) !important;
font-weight: 700;
letter-spacing: -0.02em;
} }
.prose a { .prose a {
color: var(--accent) !important; color: var(--accent) !important;
text-decoration: none;
font-weight: 500;
transition: color 0.15s ease;
}
.prose a:hover {
color: var(--accent-hover) !important;
text-decoration: underline;
} }
.prose table { .prose table {
width: 100%; width: 100%;
border-collapse: separate; border-collapse: separate;
border-spacing: 0; border-spacing: 0;
margin: 1.75rem 0; margin: 2rem 0;
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.5rem; border-radius: 0.75rem;
overflow: hidden; overflow: hidden;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
background: var(--bg-surface);
} }
.prose th, .prose th,
.prose td { .prose td {
padding: 0.75rem 1rem; padding: 0.875rem 1.125rem;
text-align: left; text-align: left;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
} }
@@ -217,7 +292,8 @@ body {
font-weight: 600; font-weight: 600;
font-size: 0.8125rem; font-size: 0.8125rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.06em;
font-family: var(--font-mono);
} }
.prose td { .prose td {
@@ -228,6 +304,10 @@ body {
border-bottom: none; border-bottom: none;
} }
.prose tbody tr:hover {
background: var(--bg-elevated-hover);
}
.prose tbody tr:nth-child(even) { .prose tbody tr:nth-child(even) {
background: var(--table-zebra); background: var(--table-zebra);
} }
@@ -237,10 +317,11 @@ body {
color: var(--pre-text) !important; color: var(--pre-text) !important;
border: 1px solid var(--border-code); border: 1px solid var(--border-code);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1rem 1.25rem; padding: 1.25rem;
overflow-x: auto; overflow-x: auto;
position: relative; position: relative;
font-family: var(--font-mono); font-family: var(--font-mono);
line-height: 1.6;
} }
.prose code { .prose code {
@@ -252,7 +333,7 @@ body {
background: var(--code-inline-bg); background: var(--code-inline-bg);
color: var(--code-inline-text); color: var(--code-inline-text);
border: 1px solid var(--code-inline-border); border: 1px solid var(--code-inline-border);
padding: 0.15rem 0.375rem; padding: 0.15rem 0.4rem;
border-radius: 0.375rem; border-radius: 0.375rem;
font-weight: 500; font-weight: 500;
} }
@@ -264,12 +345,12 @@ body {
.prose blockquote { .prose blockquote {
border-left: 3px solid var(--brand); border-left: 3px solid var(--brand);
background: rgba(94, 106, 210, 0.08); background: rgba(99, 102, 241, 0.08);
border-radius: 0 0.375rem 0.375rem 0; border-radius: 0 0.5rem 0.5rem 0;
margin: 1.5em 0; margin: 1.75em 0;
padding: 0.75em 1.25em; padding: 0.875em 1.25em;
font-style: normal; font-style: normal;
color: var(--text-muted); color: var(--text-secondary);
} }
.prose hr { .prose hr {
@@ -346,14 +427,14 @@ body {
} }
.hljs-addition { .hljs-addition {
color: #1a7f37; color: #10b981;
background-color: rgba(46, 160, 67, 0.15); background-color: rgba(16, 185, 129, 0.15);
border-radius: 2px; border-radius: 2px;
} }
.hljs-deletion { .hljs-deletion {
color: #cf222e; color: #ef4444;
background-color: rgba(248, 81, 73, 0.15); background-color: rgba(239, 68, 68, 0.15);
border-radius: 2px; border-radius: 2px;
} }
@@ -365,37 +446,50 @@ body {
font-weight: bold; font-weight: bold;
} }
/* Animations */ /* Keyframes & Animation utilities */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); } from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
.animate-fade-in { @keyframes slideDown {
animation: fadeIn 0.2s ease-out forwards; from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
} }
/* ========================================================================== @keyframes pulseGlow {
Print & PDF Export Stylesheet 0%, 100% { opacity: 0.6; }
Optimized for publication-grade PDF generation via browser / window.print() 50% { opacity: 1; filter: drop-shadow(0 0 8px var(--brand)); }
========================================================================== */ }
.animate-fade-in {
animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-slide-down {
animation: slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-pulse-glow {
animation: pulseGlow 2.5s ease-in-out infinite;
}
/* Print & PDF Export Stylesheet */
@media print { @media print {
@page { @page {
size: A4 portrait; size: A4 portrait;
margin: 18mm 15mm 18mm 15mm; margin: 18mm 15mm 18mm 15mm;
} }
/* Force exact background colors and graphics */
*, *::before, *::after { *, *::before, *::after {
-webkit-print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important; print-color-adjust: exact !important;
} }
/* Reset document canvas to white for crisp printing & ink economy */
html, body { html, body {
background-color: #ffffff !important; background-color: #ffffff !important;
color: #0f172a !important; color: #0f172a !important;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
font-size: 10pt !important; font-size: 10pt !important;
line-height: 1.5 !important; line-height: 1.5 !important;
margin: 0 !important; margin: 0 !important;
@@ -403,9 +497,9 @@ body {
width: 100% !important; width: 100% !important;
} }
/* Hide app navigation, headers, sidebars, interactive controls */
header, header,
aside, aside,
footer,
nav.site-header, nav.site-header,
.no-print, .no-print,
.export-toolbar, .export-toolbar,
@@ -413,7 +507,6 @@ body {
display: none !important; display: none !important;
} }
/* Full width container in print */
main, main,
.print-container { .print-container {
max-width: 100% !important; max-width: 100% !important;
@@ -425,7 +518,6 @@ body {
background: transparent !important; background: transparent !important;
} }
/* Page break rules */
.page-break-before, .page-break-before,
.chapter-page, .chapter-page,
.challenge-entry { .challenge-entry {
@@ -433,14 +525,6 @@ body {
page-break-before: always !important; page-break-before: always !important;
} }
.page-break-after,
.cover-page,
.scoreboard-page,
.toc-page {
break-after: page !important;
page-break-after: always !important;
}
.avoid-break-inside, .avoid-break-inside,
.code-block-wrapper, .code-block-wrapper,
.code-block-container, .code-block-container,
@@ -459,7 +543,6 @@ body {
height: auto !important; height: auto !important;
} }
/* Code block wrapping and styling in print */
.code-block-wrapper, .code-block-wrapper,
.code-block-container { .code-block-container {
break-inside: avoid !important; break-inside: avoid !important;
@@ -492,87 +575,8 @@ body {
white-space: pre-wrap !important; white-space: pre-wrap !important;
word-break: break-all !important; word-break: break-all !important;
overflow-wrap: anywhere !important; overflow-wrap: anywhere !important;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
font-size: 8.5pt !important; font-size: 8.5pt !important;
line-height: 1.4 !important; line-height: 1.4 !important;
} }
code {
background-color: #f1f5f9 !important;
color: #0f172a !important;
padding: 1pt 3pt !important;
border-radius: 3px !important;
border: 1px solid #e2e8f0 !important;
}
pre {
background-color: #f8fafc !important;
color: #0f172a !important;
border: 1px solid #cbd5e1 !important;
border-radius: 4px !important;
padding: 8pt 10pt !important;
margin: 8pt 0 !important;
}
pre code {
background-color: transparent !important;
border: none !important;
padding: 0 !important;
}
/* Typography tweaks for formal print */
.prose {
color: #0f172a !important;
font-size: 10pt !important;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
color: #020617 !important;
font-weight: 700 !important;
page-break-after: avoid !important;
break-after: avoid !important;
}
.prose p,
.prose li {
color: #334155 !important;
}
.prose a {
color: #0f172a !important;
text-decoration: underline !important;
}
.prose blockquote {
background-color: #f8fafc !important;
border-left: 3pt solid #334155 !important;
color: #334155 !important;
padding: 6pt 10pt !important;
margin: 8pt 0 !important;
}
.prose table {
border: 1px solid #cbd5e1 !important;
font-size: 9pt !important;
width: 100% !important;
border-collapse: collapse !important;
}
.prose th {
background-color: #f1f5f9 !important;
color: #0f172a !important;
font-weight: 600 !important;
border-bottom: 1.5px solid #94a3b8 !important;
padding: 4pt 6pt !important;
}
.prose td {
border-bottom: 1px solid #e2e8f0 !important;
padding: 4pt 6pt !important;
}
} }
+34 -4
View File
@@ -2,9 +2,11 @@ import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import Header from "@/components/Header"; import Header from "@/components/Header";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
import Link from "next/link";
import { Database, Activity, Sparkles, Terminal } from "lucide-react";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "MCPedia — Knowledge base for humans and AI agents", title: "MCPedia — Knowledge Base for Humans and AI Agents",
description: description:
"A content-first knowledge base. Humans read the Web UI; AI agents use the MCP server. Both share one Core.", "A content-first knowledge base. Humans read the Web UI; AI agents use the MCP server. Both share one Core.",
}; };
@@ -35,16 +37,20 @@ export default function RootLayout({
}} }}
/> />
</head> </head>
<body className="min-h-screen bg-[var(--bg-app)] text-[var(--text-secondary)] font-sans antialiased selection:bg-[var(--brand)]/30 selection:text-white flex flex-col transition-colors duration-150"> <body className="min-h-screen bg-[var(--bg-app)] text-[var(--text-secondary)] font-sans antialiased selection:bg-[var(--brand)]/30 selection:text-white flex flex-col transition-colors duration-150 tech-grid-bg relative overflow-x-hidden">
{/* Top ambient radial glow effect */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[48rem] h-64 bg-indigo-500/10 dark:bg-indigo-600/15 blur-[120px] pointer-events-none rounded-full" />
<div className="absolute top-48 left-1/4 w-80 h-80 bg-cyan-500/5 dark:bg-cyan-500/10 blur-[100px] pointer-events-none rounded-full" />
<div hidden> <div hidden>
<div id="a11y-live-region" aria-live="polite" aria-atomic="true" /> <div id="a11y-live-region" aria-live="polite" aria-atomic="true" />
</div> </div>
<Header /> <Header />
<div className="mx-auto max-w-7xl w-full flex-1 flex flex-col"> <div className="mx-auto max-w-7xl w-full flex-1 flex flex-col relative z-10">
<div className="flex flex-1"> <div className="flex flex-1">
<aside className="hidden lg:block w-72 shrink-0 border-r border-[var(--border-color)] sticky top-14 h-[calc(100vh-3.5rem)] overflow-y-auto bg-[var(--bg-app)]"> <aside className="hidden lg:block w-72 shrink-0 border-r border-[var(--border-color)] sticky top-15 h-[calc(100vh-3.75rem)] overflow-y-auto bg-[var(--bg-app)]/80 backdrop-blur-md">
<Sidebar /> <Sidebar />
</aside> </aside>
<main className="flex-1 min-w-0"> <main className="flex-1 min-w-0">
@@ -54,6 +60,30 @@ export default function RootLayout({
</main> </main>
</div> </div>
</div> </div>
{/* Global Telemetry & Status Footer */}
<footer className="no-print border-t border-[var(--border-color)] bg-[var(--bg-surface)]/60 backdrop-blur-md py-4 text-xs text-[var(--text-dim)] relative z-10">
<div className="mx-auto max-w-7xl px-4 sm:px-8 flex flex-col sm:flex-row items-center justify-between gap-3 font-mono">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span>MCPedia Intelligence Engine</span>
<span>·</span>
<span className="text-[var(--text-muted)]">PostgreSQL 16 Core</span>
</div>
<div className="flex items-center gap-4 text-[11px] text-[var(--text-muted)]">
<Link href="/search" className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] transition-colors">
RRF Hybrid Search
</Link>
<span>·</span>
<Link href="/export/pdf" className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] transition-colors">
Publication PDF
</Link>
<span>·</span>
<span className="text-[var(--text-dim)]">Native MCP :4021</span>
</div>
</div>
</footer>
</body> </body>
</html> </html>
); );
+20 -16
View File
@@ -3,6 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { Lock, Key, Eye, EyeOff, ArrowLeft, ShieldCheck } from "lucide-react";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
@@ -38,29 +39,29 @@ export default function LoginPage() {
return ( return (
<div className="flex min-h-[65vh] items-center justify-center px-4"> <div className="flex min-h-[65vh] items-center justify-center px-4">
<div className="w-full max-w-md bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-6 sm:p-8 shadow-2xl relative overflow-hidden"> <div className="w-full max-w-md bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-3xl p-6 sm:p-8 shadow-2xl relative overflow-hidden">
{/* Top decorative glow */} {/* Top decorative glow */}
<div className="absolute -top-12 left-1/2 -translate-x-1/2 w-48 h-24 bg-[var(--brand)]/20 blur-2xl pointer-events-none rounded-full" /> <div className="absolute -top-12 left-1/2 -translate-x-1/2 w-48 h-24 bg-indigo-500/20 blur-2xl pointer-events-none rounded-full" />
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="w-12 h-12 rounded-xl bg-[var(--brand)] text-white font-bold text-xl flex items-center justify-center mx-auto mb-3 shadow-lg shadow-[var(--brand)]/25"> <div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-700 text-white font-bold text-xl flex items-center justify-center mx-auto mb-3 shadow-lg shadow-indigo-500/25">
M <Lock className="w-6 h-6" />
</div> </div>
<h1 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight">Admin Authentication</h1> <h1 className="text-2xl font-extrabold text-[var(--text-primary)] tracking-tight">Admin Vault Authentication</h1>
<p className="text-xs text-[var(--text-muted)] mt-1.5"> <p className="text-xs text-[var(--text-muted)] mt-1.5">
Enter your admin password to create, edit, or delete documents. Enter your admin password to manage documents and trigger schema mutations.
</p> </p>
</div> </div>
{error && ( {error && (
<div className="p-3.5 mb-6 rounded-lg border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs"> <div className="p-3.5 mb-6 rounded-xl border border-rose-500/30 bg-rose-500/10 text-rose-500 dark:text-rose-400 text-xs font-medium animate-fade-in">
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5"> <label className="block text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1.5 font-mono">
Admin Password Admin Password
</label> </label>
<div className="relative"> <div className="relative">
@@ -69,16 +70,18 @@ export default function LoginPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password..." placeholder="Enter password..."
className="w-full px-3.5 py-2.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-1 focus:ring-[var(--brand)] transition-colors" className="w-full pl-10 pr-10 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-sm text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 transition-all"
required required
autoFocus autoFocus
/> />
<Key className="w-4 h-4 text-[var(--text-dim)] absolute left-3.5 top-3.5 pointer-events-none" />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-2.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)]" className="absolute right-3.5 top-3 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] p-1 cursor-pointer"
aria-label={showPassword ? "Hide password" : "Show password"}
> >
{showPassword ? "Hide" : "Show"} {showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button> </button>
</div> </div>
</div> </div>
@@ -86,23 +89,24 @@ export default function LoginPage() {
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg transition-all font-medium text-sm shadow-md shadow-[var(--brand)]/20 disabled:opacity-50 flex items-center justify-center gap-2" className="w-full py-3 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-xl transition-all font-semibold text-sm shadow-md shadow-[var(--brand)]/20 hover:shadow-[var(--brand)]/35 disabled:opacity-50 flex items-center justify-center gap-2 cursor-pointer"
> >
{loading && ( {loading && (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
)} )}
<span>{loading ? "Authenticating..." : "Sign In"}</span> <span>{loading ? "Authenticating Session..." : "Authorize Access"}</span>
</button> </button>
</form> </form>
<div className="mt-8 pt-6 border-t border-[var(--border-color)] flex items-center justify-between text-xs text-[var(--text-muted)]"> <div className="mt-8 pt-6 border-t border-[var(--border-color)] flex items-center justify-between text-xs text-[var(--text-muted)]">
<Link <Link
href="/" href="/"
className="hover:text-[var(--text-primary)] transition-colors" className="hover:text-[var(--text-primary)] transition-colors flex items-center gap-1"
> >
← Back to Home <ArrowLeft className="w-3.5 h-3.5" />
<span>Return to Knowledge Base</span>
</Link> </Link>
<span className="text-[11px] text-[var(--text-dim)]">HTTP-only Secure Cookie</span> <span className="text-[11px] text-[var(--text-dim)] font-mono">Secure HTTP Cookie</span>
</div> </div>
</div> </div>
</div> </div>
+123 -71
View File
@@ -2,6 +2,23 @@ import Link from "next/link";
import { listDocuments, listSections } from "@mcpedia/core"; import { listDocuments, listSections } from "@mcpedia/core";
import { getSectionMeta, type SectionConfig } from "@mcpedia/config"; import { getSectionMeta, type SectionConfig } from "@mcpedia/config";
import McpConfigSnippet from "@/components/McpConfigSnippet"; import McpConfigSnippet from "@/components/McpConfigSnippet";
import InteractiveTopology from "@/components/InteractiveTopology";
import {
Sparkles,
Search,
Plus,
BookOpen,
ArrowRight,
Database,
Cpu,
Layers,
Activity,
Folder,
FileText,
Clock,
CheckCircle2,
Terminal,
} from "lucide-react";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -22,7 +39,6 @@ function slugToTitle(segment: string): string {
function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] { function buildFolderTree(docs: { slug: string; title: string }[], section: string): TreeNode[] {
const tree: TreeNode[] = []; const tree: TreeNode[] = [];
const treeIndex = new Map<string, TreeNode>();
for (const doc of docs) { for (const doc of docs) {
const parts = doc.slug.split("/"); const parts = doc.slug.split("/");
@@ -44,7 +60,6 @@ function buildFolderTree(docs: { slug: string; title: string }[], section: strin
isLeaf: false, isLeaf: false,
}; };
current.push(node); current.push(node);
treeIndex.set(currentPath, node);
} }
if (isLeaf) { if (isLeaf) {
@@ -77,26 +92,28 @@ function buildFolderTree(docs: { slug: string; title: string }[], section: strin
return sortAll(tree); return sortAll(tree);
} }
function renderTree(nodes: TreeNode[], pathname: string) { function renderTree(nodes: TreeNode[]) {
return ( return (
<ul className="space-y-1"> <ul className="space-y-1">
{nodes.map((node) => ( {nodes.slice(0, 5).map((node) => (
<li key={node.slug}> <li key={node.slug}>
<Link <Link
href={`/${node.slug}`} href={`/${node.slug}`}
className={`flex items-center gap-1.5 text-xs py-1 px-1.5 rounded transition-all ${ className="flex items-center gap-2 text-xs py-1 px-2 rounded-md text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)] transition-colors group"
pathname === `/${node.slug}`
? "text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/15 font-medium"
: "text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]"
}`}
title={node.title} title={node.title}
> >
<span className="opacity-80">{node.isLeaf && node.children.length === 0 ? "📄" : "📁"}</span> <span className="shrink-0 text-[var(--text-dim)] group-hover:text-[var(--brand)] transition-colors">
{node.isLeaf && node.children.length === 0 ? (
<FileText className="w-3 h-3" />
) : (
<Folder className="w-3 h-3" />
)}
</span>
<span className="truncate">{node.title || node.name}</span> <span className="truncate">{node.title || node.name}</span>
</Link> </Link>
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className="ml-3.5 border-l border-[var(--border-color)] pl-2 mt-0.5"> <div className="ml-3 border-l border-[var(--border-color)] pl-2 mt-0.5">
{renderTree(node.children, pathname)} {renderTree(node.children)}
</div> </div>
)} )}
</li> </li>
@@ -105,13 +122,7 @@ function renderTree(nodes: TreeNode[], pathname: string) {
); );
} }
interface SectionTreeProps { function SectionTree({ section, docs }: { section: string; docs: { slug: string; title: string }[] }) {
section: string;
docs: { slug: string; title: string }[];
pathname: string;
}
function SectionTree({ section, docs, pathname }: SectionTreeProps) {
const tree = buildFolderTree( const tree = buildFolderTree(
docs.filter((d) => d.slug.startsWith(`${section}/`)), docs.filter((d) => d.slug.startsWith(`${section}/`)),
section, section,
@@ -120,9 +131,9 @@ function SectionTree({ section, docs, pathname }: SectionTreeProps) {
return ( return (
<div> <div>
{tree.length > 0 ? ( {tree.length > 0 ? (
renderTree(tree, pathname) renderTree(tree)
) : ( ) : (
<p className="text-xs text-[var(--text-dim)] pl-1">No documents</p> <p className="text-xs text-[var(--text-dim)] pl-2">No documents yet</p>
)} )}
</div> </div>
); );
@@ -144,77 +155,113 @@ export default async function HomePage() {
return ( return (
<div className="space-y-16"> <div className="space-y-16">
{/* Hero Section */} {/* Hero Section */}
<section className="relative pt-4 pb-2"> <section className="relative pt-2 pb-4">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--brand)]/10 border border-[var(--brand)]/25 text-[var(--brand)] dark:text-[var(--accent)] text-xs font-medium mb-6"> {/* Floating status pill */}
<span className="w-2 h-2 rounded-full bg-[var(--brand)] animate-pulse" /> <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-[var(--brand)]/10 border border-[var(--brand)]/25 text-[var(--brand)] dark:text-[var(--accent)] text-xs font-semibold mb-6 shadow-xs animate-fade-in">
<span>Model Context Protocol + PostgreSQL Core</span> <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span>Model Context Protocol · PostgreSQL Source of Truth</span>
</div> </div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold text-[var(--text-primary)] tracking-tight mb-5 leading-tight"> {/* Primary Title */}
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-[var(--text-primary)] tracking-tight mb-5 leading-[1.12]">
Knowledge Base for <br /> Knowledge Base for <br />
<span className="bg-gradient-to-r from-[var(--brand)] via-[var(--accent)] to-[var(--accent-hover)] dark:to-white bg-clip-text text-transparent"> <span className="bg-gradient-to-r from-indigo-500 via-indigo-600 to-cyan-500 dark:to-cyan-400 bg-clip-text text-transparent">
Humans and AI Agents Humans and AI Agents
</span> </span>
</h1> </h1>
{/* Subtitle */}
<p className="text-base sm:text-lg text-[var(--text-muted)] max-w-2xl leading-relaxed mb-8"> <p className="text-base sm:text-lg text-[var(--text-muted)] max-w-2xl leading-relaxed mb-8">
A high-performance, database-backed documentation system. Browse hierarchical notes and technical writeups in your browser, or connect AI coding assistants directly via native MCP tools. A high-performance technical knowledge repository. Browse structured notes and CTF writeups in your browser, or equip AI coding assistants directly via native MCP tools.
</p> </p>
{/* Quick action buttons & stats */} {/* Quick Action Buttons */}
<div className="flex flex-wrap items-center gap-3 mb-10"> <div className="flex flex-wrap items-center gap-3 mb-10">
<Link <Link
href="/docs" href="/docs"
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg font-medium text-sm transition-all shadow-md shadow-[var(--brand)]/20" className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-xl font-semibold text-sm transition-all shadow-md shadow-[var(--brand)]/25 hover:shadow-[var(--brand)]/40 hover:-translate-y-0.5 cursor-pointer"
> >
<BookOpen className="w-4 h-4" />
<span>Browse Documentation</span> <span>Browse Documentation</span>
<span>→</span> <ArrowRight className="w-4 h-4 ml-0.5" />
</Link> </Link>
<Link <Link
href="/search" href="/search"
className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg font-medium text-sm transition-all shadow-sm" className="inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-xl font-semibold text-sm transition-all shadow-xs hover:-translate-y-0.5"
> >
<svg className="w-4 h-4 text-[var(--text-muted)]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Search className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <span>Hybrid Search Console</span>
</svg>
<span>Hybrid Search</span>
</Link> </Link>
<Link <Link
href="/create" href="/create"
className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-lg text-sm transition-colors shadow-sm" className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-[var(--bg-surface)] hover:bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] text-[var(--text-muted)] hover:text-[var(--text-primary)] rounded-xl text-sm transition-all shadow-xs"
> >
<span>+ Create Document</span> <Plus className="w-4 h-4" />
<span>Create Document</span>
</Link> </Link>
</div> </div>
{/* Stats bar */} {/* Live Telemetry Metric Strip */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl max-w-3xl shadow-sm"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 p-4 sm:p-5 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-sm">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-indigo-500/10 text-indigo-600 dark:text-indigo-400">
<Database className="w-5 h-5" />
</div>
<div> <div>
<div className="text-xl font-bold text-[var(--text-primary)]">{all.length}</div> <div className="text-xl font-bold text-[var(--text-primary)]">{all.length}</div>
<div className="text-xs text-[var(--text-muted)]">Documents in DB</div> <div className="text-[11px] text-[var(--text-muted)] font-mono">Documents in DB</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-cyan-500/10 text-cyan-600 dark:text-cyan-400">
<Layers className="w-5 h-5" />
</div> </div>
<div> <div>
<div className="text-xl font-bold text-[var(--text-primary)]">{sections.length}</div> <div className="text-xl font-bold text-[var(--text-primary)]">{sections.length}</div>
<div className="text-xs text-[var(--text-muted)]">Active sections</div> <div className="text-[11px] text-[var(--text-muted)] font-mono">Active Sections</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-violet-500/10 text-violet-600 dark:text-violet-400">
<Search className="w-5 h-5" />
</div> </div>
<div> <div>
<div className="text-xl font-bold text-[var(--brand)] dark:text-[var(--accent)]">RRF Hybrid</div> <div className="text-xl font-bold text-[var(--brand)] dark:text-[var(--accent)]">RRF Hybrid</div>
<div className="text-xs text-[var(--text-muted)]">FTS + Cosine Vectors</div> <div className="text-[11px] text-[var(--text-muted)] font-mono">FTS + Cosine Vectors</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<Terminal className="w-5 h-5" />
</div> </div>
<div> <div>
<div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">13 MCP Tools</div> <div className="text-xl font-bold text-emerald-600 dark:text-emerald-400">13 Tools</div>
<div className="text-xs text-[var(--text-muted)]">Streamable HTTP :4021</div> <div className="text-[11px] text-[var(--text-muted)] font-mono">Streamable HTTP :4021</div>
</div>
</div> </div>
</div> </div>
</section> </section>
{/* Browse by Section */} {/* Interactive System Architecture & Topology */}
<section>
<InteractiveTopology />
</section>
{/* Browse Knowledge by Section */}
<section> <section>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider"> <div className="flex items-center gap-2">
Browse by Section <Layers className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
Browse Knowledge by Section
</h2> </h2>
</div> </div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{sections.map((s) => { {sections.map((s) => {
@@ -222,19 +269,19 @@ export default async function HomePage() {
return ( return (
<div <div
key={s.id} key={s.id}
className="group flex flex-col justify-between bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl p-5 transition-all shadow-sm" className="group flex flex-col justify-between bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<div> <div>
<div className="flex items-center justify-between gap-2 mb-3"> <div className="flex items-center justify-between gap-2 mb-3">
<span className="text-2xl">{s.icon}</span> <span className="text-2xl">{s.icon}</span>
<Link <Link
href={`/${s.id}`} href={`/${s.id}`}
className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] hover:underline" className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-semibold"
> >
{sectionDocs.length} docs → {sectionDocs.length} docs →
</Link> </Link>
</div> </div>
<h3 className="font-semibold text-base text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-1.5"> <h3 className="font-bold text-base text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-1.5">
{s.label} {s.label}
</h3> </h3>
<p className="text-xs text-[var(--text-muted)] mb-4 line-clamp-2 leading-relaxed"> <p className="text-xs text-[var(--text-muted)] mb-4 line-clamp-2 leading-relaxed">
@@ -242,12 +289,11 @@ export default async function HomePage() {
</p> </p>
</div> </div>
{/* Inline folder tree */} {/* Inline folder directory preview */}
<div className="pt-3 border-t border-[var(--border-color)]"> <div className="pt-3 border-t border-[var(--border-color)]">
<SectionTree <SectionTree
section={s.id} section={s.id}
docs={all.map((d) => ({ slug: d.slug, title: d.title }))} docs={all.map((d) => ({ slug: d.slug, title: d.title }))}
pathname="/"
/> />
</div> </div>
</div> </div>
@@ -256,18 +302,22 @@ export default async function HomePage() {
</div> </div>
</section> </section>
{/* Recent Documents */} {/* Recently Updated Knowledge */}
{recent.length > 0 && ( {recent.length > 0 && (
<section> <section>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider"> <div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-[var(--brand)] dark:text-[var(--accent)]" />
<h2 className="text-xs font-bold text-[var(--text-muted)] uppercase tracking-wider font-mono">
Recently Updated Knowledge Recently Updated Knowledge
</h2> </h2>
</div>
<Link <Link
href="/docs" href="/docs"
className="text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-medium" className="text-xs text-[var(--brand)] dark:text-[var(--accent)] hover:underline font-semibold flex items-center gap-1"
> >
View all ({all.length}) → <span>View all ({all.length})</span>
<ArrowRight className="w-3.5 h-3.5" />
</Link> </Link>
</div> </div>
@@ -278,11 +328,12 @@ export default async function HomePage() {
<Link <Link
key={d.slug} key={d.slug}
href={`/${d.slug}`} href={`/${d.slug}`}
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl p-4 transition-all shadow-sm" className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<div className="flex items-center justify-between gap-2 mb-2"> <div className="flex items-center justify-between gap-2 mb-2.5">
<span className="text-xs font-mono text-[var(--brand)] dark:text-[var(--accent)] uppercase"> <span className="text-[11px] font-mono font-semibold text-[var(--brand)] dark:text-[var(--accent)] uppercase flex items-center gap-1">
{sInfo?.icon} {d.section} <span>{sInfo?.icon}</span>
<span>{d.section}</span>
</span> </span>
<time className="text-[11px] text-[var(--text-dim)] font-mono"> <time className="text-[11px] text-[var(--text-dim)] font-mono">
{new Date(d.updatedAt).toLocaleDateString(undefined, { {new Date(d.updatedAt).toLocaleDateString(undefined, {
@@ -291,15 +342,15 @@ export default async function HomePage() {
})} })}
</time> </time>
</div> </div>
<h3 className="font-medium text-sm text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-2"> <h3 className="font-bold text-sm text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors line-clamp-1 mb-2">
{d.title} {d.title}
</h3> </h3>
{d.tags.length > 0 && ( {d.tags.length > 0 && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 mt-3">
{d.tags.slice(0, 3).map((t) => ( {d.tags.slice(0, 3).map((t) => (
<span <span
key={t} key={t}
className="text-[10px] px-1.5 py-0.25 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded text-[var(--text-muted)]" className="text-[10px] px-2 py-0.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-md text-[var(--text-muted)] font-mono"
> >
#{t} #{t}
</span> </span>
@@ -313,17 +364,18 @@ export default async function HomePage() {
</section> </section>
)} )}
{/* MCP Agent Integration Showcase */} {/* Connect AI Assistant Showcase */}
<section className="border-t border-[var(--border-color)] pt-12 pb-6"> <section className="border-t border-[var(--border-color)] pt-12 pb-6">
<div className="mb-6"> <div className="mb-6">
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] text-xs font-mono mb-2"> <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[var(--bg-elevated)] border border-[var(--border-color)] text-[var(--text-muted)] text-xs font-mono mb-3">
<span>⚡ Native Model Context Protocol (MCP)</span> <Sparkles className="w-3.5 h-3.5 text-indigo-500" />
<span>Connect Coding Assistants</span>
</div> </div>
<h2 className="text-2xl font-bold text-[var(--text-primary)] tracking-tight mb-2"> <h2 className="text-2xl sm:text-3xl font-extrabold text-[var(--text-primary)] tracking-tight mb-2">
Connect AI Coding Assistants Model Context Protocol Configuration
</h2> </h2>
<p className="text-sm text-[var(--text-muted)] max-w-xl"> <p className="text-xs sm:text-sm text-[var(--text-muted)] max-w-xl leading-relaxed">
Give Claude Desktop, Cursor, Antigravity, or Zed direct access to your knowledge base via semantic search, read resources, and document tools. Equip Claude Desktop, Cursor, Antigravity, or custom agents with direct access to your knowledge base via semantic search, read resources, and document tools.
</p> </p>
</div> </div>
+74 -51
View File
@@ -4,6 +4,19 @@ import { useState, useEffect, useCallback, Suspense } from "react";
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections"; import { DEFAULT_SECTIONS, type SectionConfig } from "@mcpedia/config/sections";
import {
Search,
Sparkles,
Zap,
Cpu,
Layers,
FileText,
Clock,
ArrowRight,
Filter,
X,
Compass,
} from "lucide-react";
interface DocHit { interface DocHit {
slug: string; slug: string;
@@ -78,7 +91,8 @@ function SearchContent() {
} }
} }
const filteredResults = selectedSection === "all" const filteredResults =
selectedSection === "all"
? results ? results
: results.filter((r) => r.section === selectedSection); : results.filter((r) => r.section === selectedSection);
@@ -86,65 +100,64 @@ function SearchContent() {
<div className="max-w-4xl space-y-8"> <div className="max-w-4xl space-y-8">
{/* Header */} {/* Header */}
<div> <div>
<h1 className="text-3xl font-bold text-[var(--text-primary)] tracking-tight mb-2"> <div className="flex items-center gap-2 mb-2">
Search Knowledge Base <span className="p-2 rounded-xl bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)]">
<Search className="w-5 h-5" />
</span>
<h1 className="text-3xl font-extrabold text-[var(--text-primary)] tracking-tight">
Multi-Modal Search Console
</h1> </h1>
<p className="text-sm text-[var(--text-muted)]"> </div>
Find answers across all documentation, CTF writeups, research notes, and semantic vector chunks. <p className="text-xs sm:text-sm text-[var(--text-muted)]">
Search across documentation, CTF writeups, and research notes using PostgreSQL Full-Text Search, Cosine Embeddings, or Reciprocal Rank Fusion.
</p> </p>
</div> </div>
{/* Search Box & Controls */} {/* Search Box & Controls */}
<div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl p-4 sm:p-5 shadow-sm space-y-4"> <div className="bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl p-5 sm:p-6 shadow-sm space-y-5">
{/* Search Input */} {/* Search Input Bar */}
<div className="relative"> <div className="relative">
<input <input
type="search" type="search"
value={query} value={query}
onChange={handleChange} onChange={handleChange}
placeholder="Search by keywords, challenge names, tools, concepts..." placeholder="Search by keywords, challenge names, tools, concepts..."
className="w-full pl-11 pr-10 py-3 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-lg text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 text-base transition-all" className="w-full pl-12 pr-10 py-3.5 bg-[var(--bg-elevated)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-[var(--text-primary)] placeholder-[var(--text-dim)] focus:outline-none focus:border-[var(--brand)] focus:ring-2 focus:ring-[var(--brand)]/20 text-base transition-all"
autoFocus autoFocus
/> />
<svg <Search className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] absolute left-4 top-4 pointer-events-none" />
className="w-5 h-5 text-[var(--brand)] dark:text-[var(--accent)] absolute left-3.5 top-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{query && ( {query && (
<button <button
onClick={() => { onClick={() => {
setQuery(""); setQuery("");
setResults([]); setResults([]);
}} }}
className="absolute right-3.5 top-3.5 text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] p-0.5" className="absolute right-3.5 top-3.5 p-1 text-[var(--text-dim)] hover:text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-surface)]"
aria-label="Clear query"
> >
✕ <X className="w-4 h-4" />
</button> </button>
)} )}
</div> </div>
{/* Search Mode & Section Filters */} {/* Algorithm & Filter Controls */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-2 border-t border-[var(--border-color)] text-xs"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pt-3 border-t border-[var(--border-color)] text-xs">
{/* Mode Selector */} {/* Mode Selector */}
<div className="flex items-center gap-1.5 flex-wrap"> <div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[var(--text-muted)] mr-1">Algorithm:</span> <span className="text-[var(--text-dim)] font-mono uppercase font-semibold mr-1">Algorithm:</span>
{[ {[
{ id: "hybrid", label: "⚡ Hybrid (RRF)", desc: "FTS + Vector Fusion" }, { id: "hybrid", label: "⚡ Hybrid (RRF)", desc: "Reciprocal Rank Fusion (FTS + Vector)" },
{ id: "keyword", label: "🔍 Keyword", desc: "PostgreSQL FTS" }, { id: "keyword", label: "🔍 Keyword (FTS)", desc: "PostgreSQL GIN tsvector" },
{ id: "semantic", label: "🧠 Semantic", desc: "Embeddings Cosine" }, { id: "semantic", label: "🧠 Semantic Vector", desc: "Cosine Similarity over Embeddings" },
].map((m) => ( ].map((m) => (
<button <button
key={m.id} key={m.id}
type="button" type="button"
onClick={() => handleModeChange(m.id as SearchMode)} onClick={() => handleModeChange(m.id as SearchMode)}
title={m.desc} title={m.desc}
className={`px-3 py-1 rounded-md border font-medium transition-all ${ className={`px-3 py-1.5 rounded-lg border font-semibold transition-all cursor-pointer ${
mode === m.id mode === m.id
? "bg-[var(--brand)] border-[var(--brand)] text-white shadow-sm" ? "bg-[var(--brand)] border-[var(--brand)] text-white shadow-sm shadow-[var(--brand)]/25"
: "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]" : "bg-[var(--bg-elevated)] border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
}`} }`}
> >
@@ -158,22 +171,22 @@ function SearchContent() {
<button <button
type="button" type="button"
onClick={() => setSelectedSection("all")} onClick={() => setSelectedSection("all")}
className={`px-2.5 py-1 rounded-md text-xs transition-colors ${ className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
selectedSection === "all" selectedSection === "all"
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-medium" ? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-bold"
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]" : "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
}`} }`}
> >
All Sections All
</button> </button>
{sections.map((s) => ( {sections.map((s) => (
<button <button
key={s.id} key={s.id}
type="button" type="button"
onClick={() => setSelectedSection(s.id)} onClick={() => setSelectedSection(s.id)}
className={`px-2.5 py-1 rounded-md text-xs transition-colors ${ className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
selectedSection === s.id selectedSection === s.id
? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-medium" ? "bg-[var(--bg-elevated-hover)] text-[var(--text-primary)] font-bold"
: "text-[var(--text-dim)] hover:text-[var(--text-muted)]" : "text-[var(--text-dim)] hover:text-[var(--text-muted)]"
}`} }`}
> >
@@ -184,38 +197,48 @@ function SearchContent() {
</div> </div>
</div> </div>
{/* Loading State */} {/* Loading Spinner */}
{loading && ( {loading && (
<div className="flex items-center gap-3 py-8 text-[var(--text-muted)] text-sm"> <div className="flex items-center justify-center gap-3 py-12 text-[var(--text-muted)] text-sm animate-fade-in">
<div className="w-5 h-5 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-[var(--brand)] border-t-transparent rounded-full animate-spin" />
<span>Searching knowledge base...</span> <span>Searching knowledge vector space...</span>
</div> </div>
)} )}
{/* Empty State */} {/* Empty State */}
{!loading && query && filteredResults.length === 0 && ( {!loading && query && filteredResults.length === 0 && (
<div className="text-center py-16 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl shadow-sm"> <div className="text-center py-16 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl shadow-sm space-y-3 animate-fade-in">
<div className="text-3xl mb-3">🔍</div> <div className="p-3 w-12 h-12 rounded-2xl bg-[var(--bg-elevated)] text-[var(--text-dim)] mx-auto flex items-center justify-center">
<h3 className="text-base font-semibold text-[var(--text-primary)] mb-1">No matching documents</h3> <Search className="w-6 h-6" />
</div>
<h3 className="text-base font-bold text-[var(--text-primary)]">No matching documents found</h3>
<p className="text-xs text-[var(--text-muted)] max-w-sm mx-auto"> <p className="text-xs text-[var(--text-muted)] max-w-sm mx-auto">
Try switching algorithm mode to <strong>Hybrid</strong> or <strong>Semantic</strong>, or search with different keywords. Try switching algorithm mode to <strong>Hybrid</strong> or <strong>Semantic</strong>, or search with different keywords.
</p> </p>
</div> </div>
)} )}
{/* Initial Hint */} {/* Initial Suggestions */}
{!loading && !query && ( {!loading && !query && (
<div className="p-8 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-xl space-y-4 shadow-sm"> <div className="p-6 sm:p-8 text-center bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-2xl space-y-4 shadow-sm">
<p className="text-xs text-[var(--text-muted)]">Popular search queries:</p> <p className="text-xs font-mono uppercase tracking-wider text-[var(--text-dim)]">Popular search queries:</p>
<div className="flex justify-center flex-wrap gap-2 text-xs"> <div className="flex justify-center flex-wrap gap-2 text-xs">
{["stack alignment", "ret2win", "bullmq worker", "streamable http", "full-text search", "caddy reverse proxy"].map((term) => ( {[
"stack alignment",
"ret2win exploit",
"bullmq worker",
"streamable http",
"full-text search",
"caddy reverse proxy",
"pgvector cosine",
].map((term) => (
<button <button
key={term} key={term}
onClick={() => { onClick={() => {
setQuery(term); setQuery(term);
handleSearch(term, mode); handleSearch(term, mode);
}} }}
className="px-3 py-1 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-md text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors" className="px-3.5 py-1.5 bg-[var(--bg-elevated)] hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all font-mono cursor-pointer"
> >
{term} {term}
</button> </button>
@@ -226,10 +249,10 @@ function SearchContent() {
{/* Results List */} {/* Results List */}
{!loading && filteredResults.length > 0 && ( {!loading && filteredResults.length > 0 && (
<div className="space-y-4"> <div className="space-y-4 animate-fade-in">
<div className="flex items-center justify-between text-xs text-[var(--text-dim)]"> <div className="flex items-center justify-between text-xs text-[var(--text-dim)] font-mono">
<span>Found {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""}</span> <span>Found {filteredResults.length} result{filteredResults.length !== 1 ? "s" : ""}</span>
<span className="font-mono">Ranked by {mode} score</span> <span>Ranked by {mode} score</span>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
@@ -237,29 +260,29 @@ function SearchContent() {
<Link <Link
key={r.slug} key={r.slug}
href={`/${r.slug}`} href={`/${r.slug}`}
className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-xl p-4 sm:p-5 transition-all shadow-sm" className="group block bg-[var(--bg-surface)] hover:bg-[var(--bg-surface-hover)] border border-[var(--border-color)] hover:border-[var(--brand)] rounded-2xl p-5 transition-all shadow-xs hover:shadow-md hover:-translate-y-0.5"
> >
<div className="flex items-center justify-between gap-3 mb-2"> <div className="flex items-center justify-between gap-3 mb-2.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs font-mono px-2 py-0.5 rounded bg-[var(--brand)]/10 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] uppercase"> <span className="text-[10px] font-mono px-2 py-0.5 rounded-md bg-[var(--brand)]/10 border border-[var(--brand)]/30 text-[var(--brand)] dark:text-[var(--accent)] uppercase font-bold">
{r.section} {r.section}
</span> </span>
<span className="text-xs text-[var(--text-dim)] font-mono truncate"> <span className="text-xs text-[var(--text-dim)] font-mono truncate">
/{r.slug} /{r.slug}
</span> </span>
</div> </div>
<span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2 py-0.5 rounded"> <span className="text-xs font-mono text-[var(--text-muted)] bg-[var(--bg-elevated)] border border-[var(--border-color)] px-2.5 py-0.5 rounded-md font-semibold">
Score: {r.score.toFixed(3)} Score: {r.score.toFixed(3)}
</span> </span>
</div> </div>
<h2 className="text-base font-semibold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-2"> <h2 className="text-base font-bold text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] transition-colors mb-2">
{r.title} {r.title}
</h2> </h2>
{r.snippet && ( {r.snippet && (
<p <p
className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:px-1 [&>mark]:rounded [&>mark]:font-medium" className="text-xs text-[var(--text-muted)] leading-relaxed line-clamp-2 [&>mark]:bg-[var(--brand)]/30 [&>mark]:text-[var(--text-primary)] [&>mark]:px-1 [&>mark]:rounded [&>mark]:font-semibold"
dangerouslySetInnerHTML={{ __html: r.snippet }} dangerouslySetInnerHTML={{ __html: r.snippet }}
/> />
)} )}
+1
View File
@@ -14,6 +14,7 @@
"@mcpedia/core": "workspace:*", "@mcpedia/core": "workspace:*",
"@tailwindcss/typography": "^0.5.20", "@tailwindcss/typography": "^0.5.20",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"lucide-react": "^1.33.0",
"next": "16.3.1", "next": "16.3.1",
"react": "19.2.8", "react": "19.2.8",
"react-dom": "19.2.8", "react-dom": "19.2.8",
+3
View File
@@ -72,6 +72,7 @@
"@mcpedia/core": "workspace:*", "@mcpedia/core": "workspace:*",
"@tailwindcss/typography": "^0.5.20", "@tailwindcss/typography": "^0.5.20",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"lucide-react": "^1.33.0",
"next": "16.3.1", "next": "16.3.1",
"react": "19.2.8", "react": "19.2.8",
"react-dom": "19.2.8", "react-dom": "19.2.8",
@@ -1120,6 +1121,8 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@1.33.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-MTRwMy0ZlL8Ur/vOAiJ9XGHE+kFPC7brq6MxAm0GiGXEBj0qy0jA/pG4N675oSzciO/UCdX8T+5yUQdmDeTLxg=="],
"luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],