feat(export): redesign PDF view for formal publication styling without colors or watermarks
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
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:
@@ -38,6 +38,8 @@ export default function PdfExportView({
|
|||||||
const [copiedMd, setCopiedMd] = useState(false);
|
const [copiedMd, setCopiedMd] = useState(false);
|
||||||
|
|
||||||
const sectionMeta = getSectionMeta(summary.section);
|
const sectionMeta = getSectionMeta(summary.section);
|
||||||
|
const categoriesList = summary.categories || [];
|
||||||
|
const authorsList = summary.authors || [];
|
||||||
|
|
||||||
// Available categories for filtering
|
// Available categories for filtering
|
||||||
const allCategories = useMemo(() => {
|
const allCategories = useMemo(() => {
|
||||||
@@ -101,76 +103,26 @@ export default function PdfExportView({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryBadgeClass(category: string): string {
|
|
||||||
const cat = category.toLowerCase();
|
|
||||||
if (cat.includes("web")) {
|
|
||||||
return "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/30";
|
|
||||||
}
|
|
||||||
if (cat.includes("crypto")) {
|
|
||||||
return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/30";
|
|
||||||
}
|
|
||||||
if (cat.includes("pwn") || cat.includes("binary")) {
|
|
||||||
return "bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/30";
|
|
||||||
}
|
|
||||||
if (cat.includes("reverse") || cat.includes("rev")) {
|
|
||||||
return "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30";
|
|
||||||
}
|
|
||||||
if (cat.includes("forensic") || cat.includes("dfir")) {
|
|
||||||
return "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30";
|
|
||||||
}
|
|
||||||
if (cat.includes("osint")) {
|
|
||||||
return "bg-teal-500/10 text-teal-600 dark:text-teal-400 border-teal-500/30";
|
|
||||||
}
|
|
||||||
return "bg-[var(--brand)]/10 text-[var(--brand)] dark:text-[var(--accent)] border-[var(--brand)]/30";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoryIcon(category: string): string {
|
|
||||||
const cat = category.toLowerCase();
|
|
||||||
if (cat.includes("web")) return "🌐";
|
|
||||||
if (cat.includes("crypto")) return "🔐";
|
|
||||||
if (cat.includes("pwn") || cat.includes("binary")) return "⚡";
|
|
||||||
if (cat.includes("reverse") || cat.includes("rev")) return "⚙️";
|
|
||||||
if (cat.includes("forensic") || cat.includes("dfir")) return "🔍";
|
|
||||||
if (cat.includes("osint")) return "🛰️";
|
|
||||||
if (cat.includes("mobile")) return "📱";
|
|
||||||
if (cat.includes("hardware")) return "🔌";
|
|
||||||
return "🚩";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDifficultyBadgeClass(difficulty: string): string {
|
|
||||||
const diff = difficulty.toLowerCase();
|
|
||||||
if (/easy|simple|beginner/i.test(diff)) {
|
|
||||||
return "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/30";
|
|
||||||
}
|
|
||||||
if (/medium|intermediate/i.test(diff)) {
|
|
||||||
return "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/30";
|
|
||||||
}
|
|
||||||
if (/hard|expert|advanced|insane/i.test(diff)) {
|
|
||||||
return "bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/30";
|
|
||||||
}
|
|
||||||
return "bg-[var(--bg-elevated)] text-[var(--text-muted)] border-[var(--border-color)]";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-16 print:pb-0">
|
<div className="pb-16 print:pb-0">
|
||||||
{/* =========================================================================
|
{/* =========================================================================
|
||||||
Export Control Toolbar (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-4 sm: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">
|
||||||
<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-lg text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-all shadow-xs"
|
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"
|
||||||
>
|
>
|
||||||
<span>←</span>
|
<span>←</span>
|
||||||
<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(--brand)] dark:text-[var(--accent)] font-semibold uppercase">
|
<span className="text-xs font-mono text-[var(--text-primary)] font-semibold uppercase">
|
||||||
{sectionMeta.icon} {sectionMeta.label}
|
{sectionMeta.label}
|
||||||
</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">
|
||||||
@@ -190,18 +142,13 @@ 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)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer"
|
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"
|
||||||
title="Copy compiled markdown document"
|
title="Copy compiled markdown document"
|
||||||
>
|
>
|
||||||
{copiedMd ? (
|
{copiedMd ? (
|
||||||
<>
|
<span className="text-emerald-600 font-bold">✓ Copied</span>
|
||||||
<span className="text-emerald-500 font-bold">✓ Copied</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<span>Copy Markdown</span>
|
||||||
<span>📋</span>
|
|
||||||
<span>Copy Markdown</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -209,10 +156,9 @@ 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)] hover:border-[var(--brand)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] rounded-lg transition-all shadow-xs cursor-pointer"
|
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"
|
||||||
title="Download compiled .md file"
|
title="Download compiled .md file"
|
||||||
>
|
>
|
||||||
<span>💾</span>
|
|
||||||
<span>Download .md</span>
|
<span>Download .md</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -220,7 +166,7 @@ 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-[var(--brand)] hover:bg-[var(--brand-hover)] text-white rounded-lg shadow-sm transition-all cursor-pointer hover:shadow-md active:scale-98"
|
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"
|
||||||
title="Open browser print dialog to save as PDF"
|
title="Open browser print dialog to save as PDF"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -249,10 +195,9 @@ export default function PdfExportView({
|
|||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value as ExportSortOption)}
|
onChange={(e) => setSortBy(e.target.value as ExportSortOption)}
|
||||||
aria-label="Sort documents"
|
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)] text-[var(--text-primary)] rounded-md px-2.5 py-1 text-xs focus:outline-none focus:border-[var(--brand)] cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<option value="category_points">Category & Points (CTF 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>
|
||||||
<option value="difficulty">Difficulty (Easy to Hard)</option>
|
<option value="difficulty">Difficulty (Easy to Hard)</option>
|
||||||
<option value="title">Challenge Title (A-Z)</option>
|
<option value="title">Challenge Title (A-Z)</option>
|
||||||
@@ -261,84 +206,85 @@ export default function PdfExportView({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toggle Switches */}
|
{/* Section toggles */}
|
||||||
<div className="flex items-center gap-4 flex-wrap text-[var(--text-muted)]">
|
<div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]">
|
||||||
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]">
|
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showCover}
|
checked={showCover}
|
||||||
onChange={(e) => setShowCover(e.target.checked)}
|
onChange={(e) => setShowCover(e.target.checked)}
|
||||||
className="rounded border-[var(--border-color)] text-[var(--brand)] focus:ring-[var(--brand)]"
|
className="rounded border-[var(--border-color)] text-slate-800"
|
||||||
/>
|
/>
|
||||||
<span>Cover Page</span>
|
<span>Cover Page</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{initialDocs.length > 1 && (
|
{initialDocs.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]">
|
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={showScoreboard}
|
checked={showScoreboard}
|
||||||
onChange={(e) => setShowScoreboard(e.target.checked)}
|
onChange={(e) => setShowScoreboard(e.target.checked)}
|
||||||
className="rounded border-[var(--border-color)] text-[var(--brand)] focus:ring-[var(--brand)]"
|
className="rounded border-[var(--border-color)] text-slate-800"
|
||||||
/>
|
/>
|
||||||
<span>Scoreboard Table</span>
|
<span>Scoreboard</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]">
|
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||||
<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-[var(--brand)] focus:ring-[var(--brand)]"
|
className="rounded border-[var(--border-color)] text-slate-800"
|
||||||
/>
|
/>
|
||||||
<span>Table of Contents</span>
|
<span>Table of Contents</span>
|
||||||
</label>
|
</label>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className="inline-flex items-center gap-1.5 cursor-pointer hover:text-[var(--text-primary)]">
|
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||||
<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-[var(--brand)] focus:ring-[var(--brand)]"
|
className="rounded border-[var(--border-color)] text-slate-800"
|
||||||
/>
|
/>
|
||||||
<span>Page Break per Ch</span>
|
<span>Page Break per Ch</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category Filter Chips */}
|
{/* 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">
|
<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-[11px] text-[var(--text-muted)] font-medium mr-1">Category:</span>
|
<span className="text-[var(--text-muted)] mr-1">Filter:</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedCategory("all")}
|
onClick={() => setSelectedCategory("all")}
|
||||||
className={`px-2 py-0.5 rounded text-xs font-mono transition-colors ${
|
type="button"
|
||||||
|
className={`px-2.5 py-0.5 rounded-md text-xs font-mono transition-all cursor-pointer ${
|
||||||
selectedCategory === "all"
|
selectedCategory === "all"
|
||||||
? "bg-[var(--brand)] text-white font-medium"
|
? "bg-slate-900 text-white font-semibold"
|
||||||
: "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)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All ({initialDocs.length})
|
All ({initialDocs.length})
|
||||||
</button>
|
</button>
|
||||||
{allCategories.map((cat: string) => {
|
{allCategories.map((cat) => {
|
||||||
const count = initialDocs.filter((d: Document) => extractDocCategory(d) === cat).length;
|
const count = initialDocs.filter(
|
||||||
const isActive = selectedCategory.toLowerCase() === cat.toLowerCase();
|
(d) => extractDocCategory(d).toLowerCase() === cat.toLowerCase(),
|
||||||
|
).length;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={cat}
|
key={cat}
|
||||||
onClick={() => setSelectedCategory(isActive ? "all" : cat)}
|
onClick={() => setSelectedCategory(cat)}
|
||||||
className={`px-2 py-0.5 rounded text-xs font-mono transition-colors flex items-center gap-1 ${
|
type="button"
|
||||||
isActive
|
className={`px-2.5 py-0.5 rounded-md text-xs font-mono transition-all cursor-pointer ${
|
||||||
? "bg-[var(--brand)] text-white font-medium"
|
selectedCategory.toLowerCase() === cat.toLowerCase()
|
||||||
|
? "bg-slate-900 text-white font-semibold"
|
||||||
: "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)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{getCategoryIcon(cat)}</span>
|
{cat} ({count})
|
||||||
<span>{cat}</span>
|
|
||||||
<span className="text-[10px] opacity-75 font-sans">({count})</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -347,133 +293,111 @@ export default function PdfExportView({
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* =========================================================================
|
{/* =========================================================================
|
||||||
Main Printable Document
|
Formal Printable Document Structure
|
||||||
========================================================================= */}
|
========================================================================= */}
|
||||||
<div className="print-container bg-[var(--bg-surface)] print:bg-white border border-[var(--border-color)] print:border-none rounded-2xl print:rounded-none p-6 sm:p-10 print:p-0 shadow-sm print:shadow-none space-y-12 print:space-y-0">
|
<div className="print-container bg-white text-slate-900 border border-slate-200 print:border-none rounded-xl print:rounded-none p-6 sm:p-10 print:p-0 shadow-sm print:shadow-none space-y-10 print:space-y-0">
|
||||||
|
|
||||||
{/* =======================================================================
|
{/* =======================================================================
|
||||||
1. Cover Page
|
1. Formal Cover Page
|
||||||
======================================================================= */}
|
======================================================================= */}
|
||||||
{showCover && (
|
{showCover && (
|
||||||
<section className="cover-page flex flex-col justify-between min-h-[600px] print:min-h-[92vh] p-8 sm:p-12 print:p-10 border border-[var(--border-color)] print:border-none rounded-xl print:rounded-none bg-[var(--bg-elevated)] print:bg-transparent">
|
<section className="cover-page flex flex-col justify-between min-h-[600px] print:min-h-[90vh] p-8 sm:p-12 print:p-8 border border-slate-300 print:border-slate-800 rounded-lg print:rounded-none bg-white">
|
||||||
<div>
|
<div>
|
||||||
{/* Header Badge */}
|
{/* Formal Header Line */}
|
||||||
<div className="flex items-center justify-between gap-4 mb-8">
|
<div className="border-b-2 border-slate-900 pb-3 mb-8 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-xs font-mono font-bold tracking-widest text-slate-800 uppercase">
|
||||||
<div className="w-8 h-8 rounded-lg bg-[var(--brand)] text-white flex items-center justify-center font-bold text-sm">
|
Technical Writeups & Solutions Report
|
||||||
M
|
</span>
|
||||||
</div>
|
<span className="text-xs font-mono text-slate-600 uppercase">
|
||||||
<div>
|
Scope: {summary.scope}
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-[var(--text-primary)]">
|
|
||||||
MCPedia
|
|
||||||
</span>
|
|
||||||
<p className="text-[10px] text-[var(--text-dim)] font-mono">
|
|
||||||
Knowledge Base Documentation
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-mono font-semibold bg-[var(--brand)]/15 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30">
|
|
||||||
<span>{sectionMeta.icon}</span>
|
|
||||||
<span className="uppercase">{sectionMeta.label}</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title and Subtitle */}
|
{/* Title & Subtitle */}
|
||||||
<div className="space-y-3 mb-10 pt-4">
|
<div className="space-y-3 mb-10 pt-2">
|
||||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-[var(--text-primary)] tracking-tight leading-tight">
|
<h1 className="text-3xl sm:text-4xl font-bold text-slate-950 tracking-tight leading-tight">
|
||||||
{summary.title}
|
{summary.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm sm:text-base text-[var(--text-muted)] leading-relaxed max-w-2xl">
|
<p className="text-sm text-slate-600 leading-relaxed max-w-2xl">
|
||||||
Comprehensive challenge solutions, technical writeups, and security research findings.
|
Official compilation of challenge solutions, vulnerability analysis, and exploitation procedures.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Metric Cards Grid */}
|
{/* Summary Information Table */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 my-8">
|
<div className="my-8 border border-slate-300 rounded-md overflow-hidden">
|
||||||
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
|
<table className="w-full text-xs text-left border-collapse">
|
||||||
<div className="text-2xl font-bold text-[var(--text-primary)] font-mono">
|
<tbody>
|
||||||
{filteredAndSortedDocs.length}
|
<tr className="border-b border-slate-200 bg-slate-50">
|
||||||
</div>
|
<th className="py-2.5 px-4 font-semibold text-slate-700 w-1/3 border-r border-slate-200">
|
||||||
<div className="text-xs text-[var(--text-muted)] mt-0.5">
|
Event / Document Scope
|
||||||
Challenges / Ch
|
</th>
|
||||||
</div>
|
<td className="py-2.5 px-4 font-mono text-slate-900">
|
||||||
</div>
|
{summary.scope}
|
||||||
|
</td>
|
||||||
{currentTotalPoints > 0 && (
|
</tr>
|
||||||
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
|
<tr className="border-b border-slate-200">
|
||||||
<div className="text-2xl font-bold text-[var(--brand)] dark:text-[var(--accent)] font-mono">
|
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||||
{currentTotalPoints.toLocaleString()}
|
Total Challenges Compiled
|
||||||
</div>
|
</th>
|
||||||
<div className="text-xs text-[var(--text-muted)] mt-0.5">
|
<td className="py-2.5 px-4 font-mono font-bold text-slate-900">
|
||||||
Total Points
|
{filteredAndSortedDocs.length} challenge{filteredAndSortedDocs.length !== 1 ? "s" : ""}
|
||||||
</div>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
)}
|
{currentTotalPoints > 0 && (
|
||||||
|
<tr className="border-b border-slate-200 bg-slate-50">
|
||||||
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
|
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||||
<div className="text-2xl font-bold text-[var(--text-primary)] font-mono">
|
Total Score / Points
|
||||||
{summary.categories.length}
|
</th>
|
||||||
</div>
|
<td className="py-2.5 px-4 font-mono font-bold text-slate-900">
|
||||||
<div className="text-xs text-[var(--text-muted)] mt-0.5">
|
{currentTotalPoints.toLocaleString()} pts
|
||||||
Categories
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
</div>
|
)}
|
||||||
|
<tr className="border-b border-slate-200">
|
||||||
<div className="p-4 bg-[var(--bg-surface)] print:bg-slate-50 border border-[var(--border-color)] rounded-xl">
|
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||||
<div className="text-2xl font-bold text-[var(--text-primary)] font-mono">
|
Categories Covered
|
||||||
{summary.authors.length || 1}
|
</th>
|
||||||
</div>
|
<td className="py-2.5 px-4 text-slate-900">
|
||||||
<div className="text-xs text-[var(--text-muted)] mt-0.5">
|
{categoriesList.length > 0
|
||||||
Contributors
|
? categoriesList
|
||||||
</div>
|
.map((c) => `${c.name} (${c.count})`)
|
||||||
</div>
|
.join(", ")
|
||||||
|
: "General"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr className="border-b border-slate-200 bg-slate-50">
|
||||||
|
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||||
|
Author(s) / Contributors
|
||||||
|
</th>
|
||||||
|
<td className="py-2.5 px-4 text-slate-900 font-mono">
|
||||||
|
{authorsList.length > 0
|
||||||
|
? authorsList.map((a) => `@${a}`).join(", ")
|
||||||
|
: "Contributors"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||||
|
Date of Compilation
|
||||||
|
</th>
|
||||||
|
<td className="py-2.5 px-4 text-slate-900">
|
||||||
|
{new Date(summary.generatedAt).toLocaleDateString(undefined, {
|
||||||
|
dateStyle: "long",
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Categories Distribution */}
|
|
||||||
{summary.categories.length > 0 && (
|
|
||||||
<div className="space-y-2 mt-6">
|
|
||||||
<span className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider">
|
|
||||||
Categories Covered:
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{summary.categories.map((cat: ExportCategorySummary) => (
|
|
||||||
<span
|
|
||||||
key={cat.name}
|
|
||||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-mono border ${getCategoryBadgeClass(cat.name)}`}
|
|
||||||
>
|
|
||||||
<span>{getCategoryIcon(cat.name)}</span>
|
|
||||||
<span className="font-semibold">{cat.name}:</span>
|
|
||||||
<span>{cat.count} chall{cat.count !== 1 ? "s" : ""}</span>
|
|
||||||
{cat.points > 0 && <span className="opacity-75 font-sans">({cat.points} pts)</span>}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cover Footer */}
|
{/* Cover Footer */}
|
||||||
<div className="pt-8 border-t border-[var(--border-color)] flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs text-[var(--text-muted)]">
|
<div className="pt-6 border-t border-slate-300 flex items-center justify-between text-xs text-slate-500 font-mono">
|
||||||
<div>
|
<span>CONFIDENTIAL & TECHNICAL REPORT</span>
|
||||||
{summary.authors.length > 0 && (
|
<span>
|
||||||
<p>
|
{new Date(summary.generatedAt).toLocaleDateString(undefined, {
|
||||||
<span className="font-medium text-[var(--text-secondary)]">Authors:</span>{" "}
|
dateStyle: "long",
|
||||||
{summary.authors.map((a: string) => `@${a}`).join(", ")}
|
})}
|
||||||
</p>
|
</span>
|
||||||
)}
|
|
||||||
<p className="mt-0.5 font-mono text-[11px] text-[var(--text-dim)]">
|
|
||||||
Path: {summary.scope}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="font-mono text-right sm:text-right">
|
|
||||||
<p>
|
|
||||||
{new Date(summary.generatedAt).toLocaleDateString(undefined, {
|
|
||||||
dateStyle: "long",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-[var(--text-dim)]">Generated by MCPedia</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@@ -482,31 +406,30 @@ export default function PdfExportView({
|
|||||||
2. Scoreboard / Challenge Matrix Table
|
2. Scoreboard / Challenge Matrix Table
|
||||||
======================================================================= */}
|
======================================================================= */}
|
||||||
{showScoreboard && filteredAndSortedDocs.length > 1 && (
|
{showScoreboard && filteredAndSortedDocs.length > 1 && (
|
||||||
<section className="scoreboard-page py-6 print:py-8 border-t border-[var(--border-color)] print:border-none">
|
<section className="scoreboard-page py-6 print:py-8 border-t border-slate-200 print:border-none">
|
||||||
<div className="flex items-center justify-between gap-4 mb-4">
|
<div className="flex items-center justify-between gap-4 mb-4">
|
||||||
<h2 className="text-lg font-bold text-[var(--text-primary)] tracking-tight flex items-center gap-2">
|
<h2 className="text-base font-bold text-slate-950 uppercase tracking-tight font-mono">
|
||||||
<span>📊</span>
|
1. Challenge Overview & Scoreboard Matrix
|
||||||
<span>Challenge Overview & Scoreboard</span>
|
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-xs font-mono text-[var(--text-muted)]">
|
<span className="text-xs font-mono text-slate-600">
|
||||||
{filteredAndSortedDocs.length} Total Challenges
|
{filteredAndSortedDocs.length} Total Challenges
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto border border-[var(--border-color)] rounded-xl shadow-xs">
|
<div className="border border-slate-300 rounded-md overflow-hidden">
|
||||||
<table className="w-full text-xs text-left border-collapse">
|
<table className="w-full text-xs text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-[var(--bg-elevated)] print:bg-slate-100 border-b border-[var(--border-color)] text-[var(--text-muted)] font-mono uppercase">
|
<tr className="bg-slate-100 border-b border-slate-300 text-slate-900 font-mono font-semibold uppercase">
|
||||||
<th className="py-2.5 px-3 w-12 text-center">#</th>
|
<th className="py-2 px-3 w-10 text-center border-r border-slate-200">#</th>
|
||||||
<th className="py-2.5 px-3">Challenge</th>
|
<th className="py-2 px-3 border-r border-slate-200">Challenge</th>
|
||||||
<th className="py-2.5 px-3">Category</th>
|
<th className="py-2 px-3 border-r border-slate-200">Category</th>
|
||||||
<th className="py-2.5 px-3">Difficulty</th>
|
<th className="py-2 px-3 border-r border-slate-200">Difficulty</th>
|
||||||
<th className="py-2.5 px-3 text-right">Points</th>
|
<th className="py-2 px-3 text-right border-r border-slate-200">Points</th>
|
||||||
<th className="py-2.5 px-3">Author</th>
|
<th className="py-2 px-3 border-r border-slate-200">Author</th>
|
||||||
<th className="py-2.5 px-3 text-center">Status</th>
|
<th className="py-2 px-3 text-center">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-[var(--border-color)]">
|
<tbody className="divide-y divide-slate-200 text-slate-800">
|
||||||
{filteredAndSortedDocs.map((doc, idx) => {
|
{filteredAndSortedDocs.map((doc, idx) => {
|
||||||
const chNum = idx + 1;
|
const chNum = idx + 1;
|
||||||
const cat = extractDocCategory(doc);
|
const cat = extractDocCategory(doc);
|
||||||
@@ -517,52 +440,33 @@ export default function PdfExportView({
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={doc.slug}
|
key={doc.slug}
|
||||||
className="hover:bg-[var(--bg-elevated)]/50 transition-colors"
|
className={idx % 2 === 1 ? "bg-slate-50/70" : "bg-white"}
|
||||||
>
|
>
|
||||||
<td className="py-2.5 px-3 text-center font-mono text-[var(--text-dim)]">
|
<td className="py-2 px-3 text-center font-mono text-slate-600 border-r border-slate-200">
|
||||||
{chNum}
|
{chNum}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-3 font-medium text-[var(--text-primary)]">
|
<td className="py-2 px-3 font-medium text-slate-900 border-r border-slate-200">
|
||||||
<a
|
<a
|
||||||
href={`#ch-${chNum}`}
|
href={`#ch-${chNum}`}
|
||||||
className="hover:text-[var(--brand)] dark:hover:text-[var(--accent)] hover:underline"
|
className="hover:underline text-slate-900 font-semibold"
|
||||||
>
|
>
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-3">
|
<td className="py-2 px-3 font-mono text-slate-800 border-r border-slate-200">
|
||||||
<span
|
{cat}
|
||||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-mono border ${getCategoryBadgeClass(cat)}`}
|
|
||||||
>
|
|
||||||
<span>{getCategoryIcon(cat)}</span>
|
|
||||||
<span>{cat}</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-3">
|
<td className="py-2 px-3 text-slate-700 border-r border-slate-200">
|
||||||
{diff ? (
|
{diff || "-"}
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium border ${getDifficultyBadgeClass(diff)}`}
|
|
||||||
>
|
|
||||||
{diff}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[var(--text-dim)]">-</span>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-3 text-right font-mono font-semibold text-[var(--brand)] dark:text-[var(--accent)]">
|
<td className="py-2 px-3 text-right font-mono font-semibold text-slate-900 border-r border-slate-200">
|
||||||
{pts > 0 ? `${pts} pts` : "-"}
|
{pts > 0 ? `${pts} pts` : "-"}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-3 text-[var(--text-secondary)]">
|
<td className="py-2 px-3 text-slate-700 border-r border-slate-200 font-mono">
|
||||||
{doc.author ? `@${doc.author}` : "-"}
|
{doc.author ? `@${doc.author}` : "-"}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2.5 px-3 text-center">
|
<td className="py-2 px-3 text-center font-mono text-xs">
|
||||||
{isSolved ? (
|
{isSolved ? "Solved" : "-"}
|
||||||
<span className="text-emerald-600 font-bold" title="Solved">
|
|
||||||
✓
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[var(--text-dim)]">-</span>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -577,42 +481,39 @@ export default function PdfExportView({
|
|||||||
3. Table of Contents (TOC)
|
3. Table of Contents (TOC)
|
||||||
======================================================================= */}
|
======================================================================= */}
|
||||||
{showTOC && filteredAndSortedDocs.length > 1 && (
|
{showTOC && filteredAndSortedDocs.length > 1 && (
|
||||||
<section className="toc-page py-6 print:py-8 border-t border-[var(--border-color)] print:border-none">
|
<section className="toc-page py-6 print:py-8 border-t border-slate-200 print:border-none">
|
||||||
<h2 className="text-lg font-bold text-[var(--text-primary)] tracking-tight mb-4 flex items-center gap-2">
|
<h2 className="text-base font-bold text-slate-950 uppercase tracking-tight font-mono mb-4">
|
||||||
<span>📑</span>
|
2. Table of Contents
|
||||||
<span>Table of Contents</span>
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2.5">
|
<div className="border border-slate-200 rounded-md divide-y divide-slate-200">
|
||||||
{filteredAndSortedDocs.map((doc, idx) => {
|
{filteredAndSortedDocs.map((doc, idx) => {
|
||||||
const chNum = idx + 1;
|
const chNum = idx + 1;
|
||||||
const cat = extractDocCategory(doc);
|
const cat = extractDocCategory(doc);
|
||||||
const pts = extractDocPoints(doc);
|
const pts = extractDocPoints(doc);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<div
|
||||||
key={doc.slug}
|
key={doc.slug}
|
||||||
href={`#ch-${chNum}`}
|
className="flex items-center justify-between p-2.5 text-xs text-slate-800 hover:bg-slate-50 transition-colors"
|
||||||
className="group flex items-center justify-between p-3 rounded-lg bg-[var(--bg-elevated)] print:bg-slate-50 hover:bg-[var(--bg-elevated-hover)] border border-[var(--border-color)] text-xs transition-colors"
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5 min-w-0 pr-2">
|
<div className="flex items-center gap-2 min-w-0 pr-4">
|
||||||
<span className="font-mono text-[11px] font-bold text-[var(--brand)] dark:text-[var(--accent)] bg-[var(--brand)]/10 px-1.5 py-0.5 rounded shrink-0">
|
<span className="font-mono font-bold text-slate-700 shrink-0">
|
||||||
Ch {chNum}
|
Chapter {chNum}:
|
||||||
</span>
|
</span>
|
||||||
<span className="font-medium text-[var(--text-primary)] group-hover:text-[var(--brand)] dark:group-hover:text-[var(--accent)] truncate">
|
<a
|
||||||
|
href={`#ch-${chNum}`}
|
||||||
|
className="font-medium text-slate-900 hover:underline truncate"
|
||||||
|
>
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</span>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 shrink-0 font-mono text-[11px]">
|
<div className="flex items-center gap-2 shrink-0 font-mono text-[11px] text-slate-600">
|
||||||
<span className="text-[var(--text-muted)]">{cat}</span>
|
<span>[{cat}]</span>
|
||||||
{pts > 0 && (
|
{pts > 0 && <span className="font-semibold">{pts} pts</span>}
|
||||||
<span className="text-[var(--brand)] dark:text-[var(--accent)] font-semibold">
|
|
||||||
· {pts}p
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -639,81 +540,56 @@ export default function PdfExportView({
|
|||||||
pageBreaks && idx > 0 ? "page-break-before" : ""
|
pageBreaks && idx > 0 ? "page-break-before" : ""
|
||||||
} pt-8 first:pt-0 print:pt-6`}
|
} pt-8 first:pt-0 print:pt-6`}
|
||||||
>
|
>
|
||||||
{/* Chapter Header Card */}
|
{/* Formal Chapter Header */}
|
||||||
<div className="challenge-header-card bg-[var(--bg-elevated)] print:bg-slate-50 border border-[var(--border-color)] print:border-slate-300 rounded-xl p-5 sm:p-6 mb-6 shadow-xs">
|
<header className="challenge-header-card border-b-2 border-slate-800 pb-3 mb-6">
|
||||||
{/* Chapter index label */}
|
{/* Chapter index label */}
|
||||||
<div className="flex items-center justify-between gap-3 mb-2 flex-wrap">
|
<div className="flex items-center justify-between gap-3 text-xs font-mono text-slate-600 mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<span className="font-bold tracking-wider uppercase text-slate-800">
|
||||||
<span className="px-2 py-0.5 rounded text-xs font-mono font-bold bg-[var(--brand)] text-white">
|
CHAPTER {chNum} · CHALLENGE {chNum} OF {filteredAndSortedDocs.length}
|
||||||
CHAPTER {chNum}
|
</span>
|
||||||
</span>
|
<span className="text-[11px] text-slate-500">
|
||||||
<span className="text-xs font-mono text-[var(--text-dim)]">
|
|
||||||
Challenge {chNum} of {filteredAndSortedDocs.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-xs font-mono text-[var(--text-dim)]">
|
|
||||||
{doc.slug}
|
{doc.slug}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Challenge Title */}
|
{/* Challenge Title */}
|
||||||
<h2 className="text-2xl sm:text-3xl font-bold text-[var(--text-primary)] tracking-tight leading-snug mb-4">
|
<h2 className="text-2xl sm:text-3xl font-bold text-slate-950 tracking-tight leading-snug mb-3">
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Metadata Chips Bar */}
|
{/* Formal Metadata Line */}
|
||||||
<div className="badge-container flex flex-wrap items-center gap-2 pt-3 border-t border-[var(--border-color)]">
|
<div className="badge-container flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-mono text-slate-700 pt-1">
|
||||||
{/* Category Chip */}
|
<span>
|
||||||
<span
|
<strong className="text-slate-900">Category:</strong> {cat}
|
||||||
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-mono font-semibold border ${getCategoryBadgeClass(cat)}`}
|
|
||||||
>
|
|
||||||
<span>{getCategoryIcon(cat)}</span>
|
|
||||||
<span>Category: {cat}</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Points Chip */}
|
|
||||||
{pts > 0 && (
|
{pts > 0 && (
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-mono font-semibold bg-[var(--brand)]/10 text-[var(--brand)] dark:text-[var(--accent)] border border-[var(--brand)]/30">
|
<span>
|
||||||
<span>🎯</span>
|
<strong className="text-slate-900">Points:</strong> {pts} pts
|
||||||
<span>{pts} pts</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Difficulty Chip */}
|
|
||||||
{diff && (
|
{diff && (
|
||||||
<span
|
<span>
|
||||||
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium border ${getDifficultyBadgeClass(diff)}`}
|
<strong className="text-slate-900">Difficulty:</strong> {diff}
|
||||||
>
|
|
||||||
<span>Difficulty: {diff}</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Solved Status */}
|
|
||||||
{isSolved && (
|
{isSolved && (
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
|
<span>
|
||||||
<span>✓</span>
|
<strong className="text-slate-900">Status:</strong> Solved
|
||||||
<span>Solved</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Author Chip */}
|
|
||||||
{doc.author && (
|
{doc.author && (
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-secondary)]">
|
<span>
|
||||||
<span>👤</span>
|
<strong className="text-slate-900">Author:</strong> @{doc.author}
|
||||||
<span>@{doc.author}</span>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Date Chip */}
|
|
||||||
{doc.updatedAt && (
|
{doc.updatedAt && (
|
||||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-dim)] font-mono">
|
<span>
|
||||||
<span>📅</span>
|
<strong className="text-slate-900">Date:</strong>{" "}
|
||||||
<span>{new Date(doc.updatedAt).toLocaleDateString()}</span>
|
{new Date(doc.updatedAt).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Custom Extra Fields badges (excluding already rendered keys) */}
|
{/* Extra fields */}
|
||||||
{Object.entries(extra).map(([k, v]) => {
|
{Object.entries(extra).map(([k, v]) => {
|
||||||
const lowerKey = k.toLowerCase();
|
const lowerKey = k.toLowerCase();
|
||||||
if (
|
if (
|
||||||
@@ -734,27 +610,21 @@ export default function PdfExportView({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span
|
<span key={k}>
|
||||||
key={k}
|
<strong className="text-slate-900">{k}:</strong> {String(v)}
|
||||||
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-muted)] font-mono"
|
|
||||||
>
|
|
||||||
<span className="text-[var(--text-dim)]">{k}:</span>
|
|
||||||
<span className="text-[var(--text-secondary)] font-semibold">
|
|
||||||
{String(v)}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
{/* Markdown Writeup Body */}
|
{/* Markdown Writeup Body */}
|
||||||
<div className="prose-content mb-10">
|
<div className="prose-content mb-10 text-slate-900">
|
||||||
<Markdown source={doc.body} />
|
<Markdown source={doc.body} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chapter End Divider */}
|
{/* Chapter End Divider (screen only) */}
|
||||||
<div className="print:hidden border-b border-[var(--border-color)] my-8" />
|
<div className="print:hidden border-b border-slate-200 my-8" />
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+45
-13
@@ -382,7 +382,7 @@ body {
|
|||||||
@media print {
|
@media print {
|
||||||
@page {
|
@page {
|
||||||
size: A4 portrait;
|
size: A4 portrait;
|
||||||
margin: 15mm 15mm 15mm 15mm;
|
margin: 18mm 15mm 18mm 15mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Force exact background colors and graphics */
|
/* Force exact background colors and graphics */
|
||||||
@@ -395,7 +395,8 @@ body {
|
|||||||
html, body {
|
html, body {
|
||||||
background-color: #ffffff !important;
|
background-color: #ffffff !important;
|
||||||
color: #0f172a !important;
|
color: #0f172a !important;
|
||||||
font-size: 10.5pt !important;
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||||
|
font-size: 10pt !important;
|
||||||
line-height: 1.5 !important;
|
line-height: 1.5 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
@@ -421,6 +422,7 @@ body {
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page break rules */
|
/* Page break rules */
|
||||||
@@ -442,12 +444,19 @@ body {
|
|||||||
.challenge-header-card,
|
.challenge-header-card,
|
||||||
.badge-container,
|
.badge-container,
|
||||||
table,
|
table,
|
||||||
|
tr,
|
||||||
|
img,
|
||||||
pre,
|
pre,
|
||||||
blockquote {
|
blockquote {
|
||||||
break-inside: avoid !important;
|
break-inside: avoid !important;
|
||||||
page-break-inside: avoid !important;
|
page-break-inside: avoid !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Code block wrapping and styling in print */
|
/* Code block wrapping and styling in print */
|
||||||
pre,
|
pre,
|
||||||
pre code,
|
pre code,
|
||||||
@@ -455,30 +464,48 @@ 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-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 {
|
pre {
|
||||||
background-color: #f8fafc !important;
|
background-color: #f8fafc !important;
|
||||||
color: #0f172a !important;
|
color: #0f172a !important;
|
||||||
border: 1px solid #cbd5e1 !important;
|
border: 1px solid #cbd5e1 !important;
|
||||||
border-radius: 6px !important;
|
border-radius: 4px !important;
|
||||||
padding: 8pt 10pt !important;
|
padding: 8pt 10pt !important;
|
||||||
margin: 10pt 0 !important;
|
margin: 8pt 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography tweaks for high-contrast print */
|
pre code {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography tweaks for formal print */
|
||||||
.prose {
|
.prose {
|
||||||
color: #1e293b !important;
|
color: #0f172a !important;
|
||||||
font-size: 10.5pt !important;
|
font-size: 10pt !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h1,
|
.prose h1,
|
||||||
.prose h2,
|
.prose h2,
|
||||||
.prose h3,
|
.prose h3,
|
||||||
.prose h4 {
|
.prose h4,
|
||||||
color: #0f172a !important;
|
.prose h5,
|
||||||
|
.prose h6 {
|
||||||
|
color: #020617 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
page-break-after: avoid !important;
|
page-break-after: avoid !important;
|
||||||
break-after: avoid !important;
|
break-after: avoid !important;
|
||||||
}
|
}
|
||||||
@@ -489,30 +516,35 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose a {
|
.prose a {
|
||||||
color: #312e81 !important;
|
color: #0f172a !important;
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Links display clean href only if needed or retain normal link text */
|
|
||||||
.prose blockquote {
|
.prose blockquote {
|
||||||
background-color: #f1f5f9 !important;
|
background-color: #f8fafc !important;
|
||||||
border-left: 3.5pt solid #4f46e5 !important;
|
border-left: 3pt solid #334155 !important;
|
||||||
color: #334155 !important;
|
color: #334155 !important;
|
||||||
padding: 6pt 10pt !important;
|
padding: 6pt 10pt !important;
|
||||||
|
margin: 8pt 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose table {
|
.prose table {
|
||||||
border: 1px solid #cbd5e1 !important;
|
border: 1px solid #cbd5e1 !important;
|
||||||
font-size: 9pt !important;
|
font-size: 9pt !important;
|
||||||
|
width: 100% !important;
|
||||||
|
border-collapse: collapse !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose th {
|
.prose th {
|
||||||
background-color: #f1f5f9 !important;
|
background-color: #f1f5f9 !important;
|
||||||
color: #0f172a !important;
|
color: #0f172a !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
border-bottom: 1.5px solid #94a3b8 !important;
|
border-bottom: 1.5px solid #94a3b8 !important;
|
||||||
|
padding: 4pt 6pt !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose td {
|
.prose td {
|
||||||
border-bottom: 1px solid #e2e8f0 !important;
|
border-bottom: 1px solid #e2e8f0 !important;
|
||||||
|
padding: 4pt 6pt !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ export function compileExportMarkdown(exportData: ExportData): string {
|
|||||||
// Cover / Header
|
// Cover / Header
|
||||||
lines.push(`# ${summary.title}`);
|
lines.push(`# ${summary.title}`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push(`*Generated from MCPedia Knowledge Base on ${new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })}*`);
|
lines.push(`*Date: ${new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })}*`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
if (summary.totalDocuments > 1) {
|
if (summary.totalDocuments > 1) {
|
||||||
|
|||||||
Reference in New Issue
Block a user