From 4f0d2da8768ad005d4db492d22b769aef70ee2e5 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sat, 22 Aug 2026 13:49:00 +0700 Subject: [PATCH] feat(export): simplify writeup export to pure human-like markdown layout without cards and exclude _index documents --- apps/web/app/components/PdfExportView.tsx | 388 +++++----------------- apps/web/app/globals.css | 3 +- packages/core/src/export.service.ts | 20 +- 3 files changed, 111 insertions(+), 300 deletions(-) diff --git a/apps/web/app/components/PdfExportView.tsx b/apps/web/app/components/PdfExportView.tsx index 83088e0..5a34324 100644 --- a/apps/web/app/components/PdfExportView.tsx +++ b/apps/web/app/components/PdfExportView.tsx @@ -7,13 +7,11 @@ import { extractDocCategory, extractDocPoints, extractDocDifficulty, - extractDocSolved, sortExportDocuments, compileExportMarkdown, type Document, type ExportData, type ExportSortOption, - type ExportCategorySummary, } from "@mcpedia/core/export-helpers"; import { getSectionMeta } from "@mcpedia/config/sections"; @@ -28,38 +26,49 @@ export default function PdfExportView({ }: PdfExportViewProps) { const { summary, documents: initialDocs } = exportData; + // Filter out any leftover index / meta files + const cleanDocs = useMemo(() => { + return initialDocs.filter((d) => { + const slug = d.slug.toLowerCase(); + return ( + !slug.endsWith("/_index") && + !slug.endsWith("/index") && + slug !== "_index" && + slug !== "index" && + d.type !== "writeup-index" + ); + }); + }, [initialDocs]); + // Configuration and interactive state const [sortBy, setSortBy] = useState("category_points"); - const [showCover, setShowCover] = useState(true); - const [showScoreboard, setShowScoreboard] = useState(initialDocs.length > 1); - const [showTOC, setShowTOC] = useState(initialDocs.length > 1); + const [showTOC, setShowTOC] = useState(cleanDocs.length > 1); const [pageBreaks, setPageBreaks] = useState(true); const [selectedCategory, setSelectedCategory] = useState("all"); const [copiedMd, setCopiedMd] = useState(false); const sectionMeta = getSectionMeta(summary.section); - const categoriesList = summary.categories || []; const authorsList = summary.authors || []; // Available categories for filtering const allCategories = useMemo(() => { const set = new Set(); - for (const doc of initialDocs) { + for (const doc of cleanDocs) { set.add(extractDocCategory(doc)); } return Array.from(set).sort(); - }, [initialDocs]); + }, [cleanDocs]); // Filtered & Sorted documents const filteredAndSortedDocs = useMemo(() => { - let docs = [...initialDocs]; + let docs = [...cleanDocs]; if (selectedCategory !== "all") { docs = docs.filter( (d) => extractDocCategory(d).toLowerCase() === selectedCategory.toLowerCase(), ); } return sortExportDocuments(docs, sortBy); - }, [initialDocs, selectedCategory, sortBy]); + }, [cleanDocs, selectedCategory, sortBy]); // Calculate dynamic stats for currently filtered view const currentTotalPoints = useMemo(() => { @@ -208,38 +217,16 @@ export default function PdfExportView({ {/* Section toggles */}
- - - {initialDocs.length > 1 && ( - <> - - - - + {cleanDocs.length > 1 && ( + )}
@@ -267,10 +254,10 @@ export default function PdfExportView({ : "bg-[var(--bg-elevated)] text-[var(--text-muted)] hover:text-[var(--text-primary)] border border-[var(--border-color)]" }`} > - All ({initialDocs.length}) + All ({cleanDocs.length}) {allCategories.map((cat) => { - const count = initialDocs.filter( + const count = cleanDocs.filter( (d) => extractDocCategory(d).toLowerCase() === cat.toLowerCase(), ).length; return ( @@ -293,281 +280,85 @@ export default function PdfExportView({ {/* ========================================================================= - Formal Printable Document Structure + Clean, Simple, Human-Written Writeup Document Flow (No Cards) ========================================================================= */} -
+
- {/* ======================================================================= - 1. Formal Cover Page - ======================================================================= */} - {showCover && ( -
-
- {/* Title & Subtitle */} -
-

- {summary.title} -

-

- Official compilation of challenge solutions, vulnerability analysis, and exploitation procedures. -

-
+ {/* Document Header */} +
+

+ {summary.title} +

+
+ Scope: {summary.scope} + Total Challenges: {filteredAndSortedDocs.length} + {currentTotalPoints > 0 && ( + Total Points: {currentTotalPoints.toLocaleString()} pts + )} + {authorsList.length > 0 && ( + Author(s): {authorsList.map((a) => `@${a}`).join(", ")} + )} + Date: {new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })} +
+
- {/* Summary Information Table */} -
- - - - - - - - - - - {currentTotalPoints > 0 && ( - - - - - )} - - - - - - - - - - - - - -
- Event / Document Scope - - {summary.scope} -
- Total Challenges Compiled - - {filteredAndSortedDocs.length} challenge{filteredAndSortedDocs.length !== 1 ? "s" : ""} -
- Total Score / Points - - {currentTotalPoints.toLocaleString()} pts -
- Categories Covered - - {categoriesList.length > 0 - ? categoriesList - .map((c) => `${c.name} (${c.count})`) - .join(", ") - : "General"} -
- Author(s) / Contributors - - {authorsList.length > 0 - ? authorsList.map((a) => `@${a}`).join(", ") - : "Contributors"} -
- Date of Compilation - - {new Date(summary.generatedAt).toLocaleDateString(undefined, { - dateStyle: "long", - })} -
-
-
-
- )} - - {/* ======================================================================= - 2. Scoreboard / Challenge Matrix Table - ======================================================================= */} - {showScoreboard && filteredAndSortedDocs.length > 1 && ( -
-
-

- 1. Challenge Overview & Scoreboard Matrix -

- - {filteredAndSortedDocs.length} Total Challenges - -
- -
- - - - - - - - - - - - - - {filteredAndSortedDocs.map((doc, idx) => { - const chNum = idx + 1; - const cat = extractDocCategory(doc); - const pts = extractDocPoints(doc); - const diff = extractDocDifficulty(doc); - const isSolved = extractDocSolved(doc); - - return ( - - - - - - - - - - ); - })} - -
#ChallengeCategoryDifficultyPointsAuthorStatus
- {chNum} - - - {doc.title} - - - {cat} - - {diff || "-"} - - {pts > 0 ? `${pts} pts` : "-"} - - {doc.author ? `@${doc.author}` : "-"} - - {isSolved ? "Solved" : "-"} -
-
-
- )} - - {/* ======================================================================= - 3. Table of Contents (TOC) - ======================================================================= */} + {/* Table of Contents (Simple & Clean) */} {showTOC && filteredAndSortedDocs.length > 1 && ( -
-

- 2. Table of Contents +
+

+ Table of Contents

- -
+
    {filteredAndSortedDocs.map((doc, idx) => { - const chNum = idx + 1; const cat = extractDocCategory(doc); const pts = extractDocPoints(doc); return ( -
    -
    - - Chapter {chNum}: - - - {doc.title} - -
    - -
    - [{cat}] - {pts > 0 && {pts} pts} -
    -
    +
  1. + + {doc.title} + + + [{cat}{pts > 0 ? ` · ${pts} pts` : ""}] + +
  2. ); })} -
+
)} - {/* ======================================================================= - 4. Challenge Chapters ("Urutan Rapih Per Ch") - ======================================================================= */} -
+ {/* Challenges / Writeups Flow ("Mirip Writeup Manusia") */} +
{filteredAndSortedDocs.map((doc, idx) => { const chNum = idx + 1; const cat = extractDocCategory(doc); const pts = extractDocPoints(doc); const diff = extractDocDifficulty(doc); - const isSolved = extractDocSolved(doc); const extra = doc.extraFields || {}; return (
0 ? "page-break-before" : "" - } pt-8 first:pt-0 print:pt-6`} + } pt-6 first:pt-0`} > - {/* Formal Chapter Header */} -
- {/* Chapter index label */} -
- - CHAPTER {chNum} · CHALLENGE {chNum} OF {filteredAndSortedDocs.length} - - - {doc.slug} - -
- - {/* Challenge Title */} -

- {doc.title} + {/* Challenge Title & Metadata */} +
+

+ {chNum}. {doc.title}

- - {/* Formal Metadata Line */} -
- - Category: {cat} - - {pts > 0 && ( - - Points: {pts} pts - - )} - {diff && ( - - Difficulty: {diff} - - )} - {isSolved && ( - - Status: Solved - - )} - {doc.author && ( - - Author: @{doc.author} - - )} - {doc.updatedAt && ( - - Date:{" "} - {new Date(doc.updatedAt).toLocaleDateString()} - - )} +
+ Category: {cat} + {pts > 0 && Points: {pts} pts} + {diff && Difficulty: {diff}} + {doc.author && Author: @{doc.author}} + {doc.slug && Slug: {doc.slug}} {/* Extra fields */} {Object.entries(extra).map(([k, v]) => { @@ -582,6 +373,7 @@ export default function PdfExportView({ "solved", "author", "event", + "challenge", ].includes(lowerKey) || v === null || v === undefined || @@ -591,25 +383,25 @@ export default function PdfExportView({ } return ( - {k}: {String(v)} + {k}: {String(v)} ); })}
-

+
- {/* Markdown Writeup Body */} -
+ {/* Markdown Writeup Content */} +
- {/* Chapter End Divider (screen only) */} -
+ {/* Divider between challenges (screen only) */} +
); })}
-
+

); } diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 603c30d..a005d4d 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -427,7 +427,8 @@ body { /* Page break rules */ .page-break-before, - .chapter-page { + .chapter-page, + .challenge-entry { break-before: page !important; page-break-before: always !important; } diff --git a/packages/core/src/export.service.ts b/packages/core/src/export.service.ts index deca57a..17c358d 100644 --- a/packages/core/src/export.service.ts +++ b/packages/core/src/export.service.ts @@ -104,8 +104,26 @@ export async function getExportDocuments( return { ...toMeta(row), body: body || "" }; }); + // Filter out meta index documents (like _index or index) when exporting multiple docs + let filteredDocs = loadedDocs; + if (loadedDocs.length > 1) { + const withoutIndex = loadedDocs.filter((d) => { + const slug = d.slug.toLowerCase(); + return ( + !slug.endsWith("/_index") && + !slug.endsWith("/index") && + slug !== "_index" && + slug !== "index" && + d.type !== "writeup-index" + ); + }); + if (withoutIndex.length > 0) { + filteredDocs = withoutIndex; + } + } + // Sort neatly - const sortedDocs = sortExportDocuments(loadedDocs, sortBy); + const sortedDocs = sortExportDocuments(filteredDocs, sortBy); const summary = buildExportSummary(sortedDocs, opts.path, opts.section); return {