feat(export): simplify writeup export to pure human-like markdown layout without cards and exclude _index documents
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:
@@ -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<ExportSortOption>("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<string>("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<string>();
|
||||
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 */}
|
||||
<div className="flex items-center gap-3 flex-wrap text-[var(--text-secondary)]">
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showCover}
|
||||
onChange={(e) => setShowCover(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-slate-800"
|
||||
/>
|
||||
<span>Cover Page</span>
|
||||
</label>
|
||||
|
||||
{initialDocs.length > 1 && (
|
||||
<>
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showScoreboard}
|
||||
onChange={(e) => setShowScoreboard(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-slate-800"
|
||||
/>
|
||||
<span>Scoreboard</span>
|
||||
</label>
|
||||
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showTOC}
|
||||
onChange={(e) => setShowTOC(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-slate-800"
|
||||
/>
|
||||
<span>Table of Contents</span>
|
||||
</label>
|
||||
</>
|
||||
{cleanDocs.length > 1 && (
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showTOC}
|
||||
onChange={(e) => setShowTOC(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-slate-800"
|
||||
/>
|
||||
<span>Table of Contents</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
@@ -249,7 +236,7 @@ export default function PdfExportView({
|
||||
onChange={(e) => setPageBreaks(e.target.checked)}
|
||||
className="rounded border-[var(--border-color)] text-slate-800"
|
||||
/>
|
||||
<span>Page Break per Ch</span>
|
||||
<span>Page Break per Challenge</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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})
|
||||
</button>
|
||||
{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({
|
||||
</aside>
|
||||
|
||||
{/* =========================================================================
|
||||
Formal Printable Document Structure
|
||||
Clean, Simple, Human-Written Writeup Document Flow (No Cards)
|
||||
========================================================================= */}
|
||||
<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">
|
||||
<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">
|
||||
|
||||
{/* =======================================================================
|
||||
1. Formal Cover Page
|
||||
======================================================================= */}
|
||||
{showCover && (
|
||||
<section className="cover-page flex flex-col justify-center 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>
|
||||
{/* Title & Subtitle */}
|
||||
<div className="space-y-3 mb-8">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-slate-950 tracking-tight leading-tight">
|
||||
{summary.title}
|
||||
</h1>
|
||||
<p className="text-sm text-slate-600 leading-relaxed max-w-2xl">
|
||||
Official compilation of challenge solutions, vulnerability analysis, and exploitation procedures.
|
||||
</p>
|
||||
</div>
|
||||
{/* Document Header */}
|
||||
<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">
|
||||
{summary.title}
|
||||
</h1>
|
||||
<div className="text-xs text-slate-600 font-mono flex flex-wrap gap-x-4 gap-y-1">
|
||||
<span><strong>Scope:</strong> {summary.scope}</span>
|
||||
<span><strong>Total Challenges:</strong> {filteredAndSortedDocs.length}</span>
|
||||
{currentTotalPoints > 0 && (
|
||||
<span><strong>Total Points:</strong> {currentTotalPoints.toLocaleString()} pts</span>
|
||||
)}
|
||||
{authorsList.length > 0 && (
|
||||
<span><strong>Author(s):</strong> {authorsList.map((a) => `@${a}`).join(", ")}</span>
|
||||
)}
|
||||
<span><strong>Date:</strong> {new Date(summary.generatedAt).toLocaleDateString(undefined, { dateStyle: "long" })}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Summary Information Table */}
|
||||
<div className="my-6 border border-slate-300 rounded-md overflow-hidden">
|
||||
<table className="w-full text-xs text-left border-collapse">
|
||||
<tbody>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="py-2.5 px-4 font-semibold text-slate-700 w-1/3 border-r border-slate-200">
|
||||
Event / Document Scope
|
||||
</th>
|
||||
<td className="py-2.5 px-4 font-mono text-slate-900">
|
||||
{summary.scope}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||
Total Challenges Compiled
|
||||
</th>
|
||||
<td className="py-2.5 px-4 font-mono font-bold text-slate-900">
|
||||
{filteredAndSortedDocs.length} challenge{filteredAndSortedDocs.length !== 1 ? "s" : ""}
|
||||
</td>
|
||||
</tr>
|
||||
{currentTotalPoints > 0 && (
|
||||
<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">
|
||||
Total Score / Points
|
||||
</th>
|
||||
<td className="py-2.5 px-4 font-mono font-bold text-slate-900">
|
||||
{currentTotalPoints.toLocaleString()} pts
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="py-2.5 px-4 font-semibold text-slate-700 border-r border-slate-200">
|
||||
Categories Covered
|
||||
</th>
|
||||
<td className="py-2.5 px-4 text-slate-900">
|
||||
{categoriesList.length > 0
|
||||
? categoriesList
|
||||
.map((c) => `${c.name} (${c.count})`)
|
||||
.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>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* =======================================================================
|
||||
2. Scoreboard / Challenge Matrix Table
|
||||
======================================================================= */}
|
||||
{showScoreboard && filteredAndSortedDocs.length > 1 && (
|
||||
<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">
|
||||
<h2 className="text-base font-bold text-slate-950 uppercase tracking-tight font-mono">
|
||||
1. Challenge Overview & Scoreboard Matrix
|
||||
</h2>
|
||||
<span className="text-xs font-mono text-slate-600">
|
||||
{filteredAndSortedDocs.length} Total Challenges
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-300 rounded-md overflow-hidden">
|
||||
<table className="w-full text-xs text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-100 border-b border-slate-300 text-slate-900 font-mono font-semibold uppercase">
|
||||
<th className="py-2 px-3 w-10 text-center border-r border-slate-200">#</th>
|
||||
<th className="py-2 px-3 border-r border-slate-200">Challenge</th>
|
||||
<th className="py-2 px-3 border-r border-slate-200">Category</th>
|
||||
<th className="py-2 px-3 border-r border-slate-200">Difficulty</th>
|
||||
<th className="py-2 px-3 text-right border-r border-slate-200">Points</th>
|
||||
<th className="py-2 px-3 border-r border-slate-200">Author</th>
|
||||
<th className="py-2 px-3 text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200 text-slate-800">
|
||||
{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 (
|
||||
<tr
|
||||
key={doc.slug}
|
||||
className={idx % 2 === 1 ? "bg-slate-50/70" : "bg-white"}
|
||||
>
|
||||
<td className="py-2 px-3 text-center font-mono text-slate-600 border-r border-slate-200">
|
||||
{chNum}
|
||||
</td>
|
||||
<td className="py-2 px-3 font-medium text-slate-900 border-r border-slate-200">
|
||||
<a
|
||||
href={`#ch-${chNum}`}
|
||||
className="hover:underline text-slate-900 font-semibold"
|
||||
>
|
||||
{doc.title}
|
||||
</a>
|
||||
</td>
|
||||
<td className="py-2 px-3 font-mono text-slate-800 border-r border-slate-200">
|
||||
{cat}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-700 border-r border-slate-200">
|
||||
{diff || "-"}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-mono font-semibold text-slate-900 border-r border-slate-200">
|
||||
{pts > 0 ? `${pts} pts` : "-"}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-700 border-r border-slate-200 font-mono">
|
||||
{doc.author ? `@${doc.author}` : "-"}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-center font-mono text-xs">
|
||||
{isSolved ? "Solved" : "-"}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* =======================================================================
|
||||
3. Table of Contents (TOC)
|
||||
======================================================================= */}
|
||||
{/* Table of Contents (Simple & Clean) */}
|
||||
{showTOC && filteredAndSortedDocs.length > 1 && (
|
||||
<section className="toc-page py-6 print:py-8 border-t border-slate-200 print:border-none">
|
||||
<h2 className="text-base font-bold text-slate-950 uppercase tracking-tight font-mono mb-4">
|
||||
2. Table of Contents
|
||||
<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">
|
||||
Table of Contents
|
||||
</h2>
|
||||
|
||||
<div className="border border-slate-200 rounded-md divide-y divide-slate-200">
|
||||
<ol className="list-decimal list-inside space-y-1.5 text-xs text-slate-800">
|
||||
{filteredAndSortedDocs.map((doc, idx) => {
|
||||
const chNum = idx + 1;
|
||||
const cat = extractDocCategory(doc);
|
||||
const pts = extractDocPoints(doc);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={doc.slug}
|
||||
className="flex items-center justify-between p-2.5 text-xs text-slate-800 hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 pr-4">
|
||||
<span className="font-mono font-bold text-slate-700 shrink-0">
|
||||
Chapter {chNum}:
|
||||
</span>
|
||||
<a
|
||||
href={`#ch-${chNum}`}
|
||||
className="font-medium text-slate-900 hover:underline truncate"
|
||||
>
|
||||
{doc.title}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0 font-mono text-[11px] text-slate-600">
|
||||
<span>[{cat}]</span>
|
||||
{pts > 0 && <span className="font-semibold">{pts} pts</span>}
|
||||
</div>
|
||||
</div>
|
||||
<li key={doc.slug}>
|
||||
<a
|
||||
href={`#ch-${idx + 1}`}
|
||||
className="hover:underline font-medium text-slate-900"
|
||||
>
|
||||
{doc.title}
|
||||
</a>
|
||||
<span className="text-slate-500 font-mono ml-2">
|
||||
[{cat}{pts > 0 ? ` · ${pts} pts` : ""}]
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* =======================================================================
|
||||
4. Challenge Chapters ("Urutan Rapih Per Ch")
|
||||
======================================================================= */}
|
||||
<div className="space-y-12 print:space-y-0">
|
||||
{/* Challenges / Writeups Flow ("Mirip Writeup Manusia") */}
|
||||
<div className="challenges-flow space-y-10 print:space-y-0">
|
||||
{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 (
|
||||
<article
|
||||
key={doc.slug}
|
||||
id={`ch-${chNum}`}
|
||||
className={`chapter-page ${
|
||||
className={`challenge-entry ${
|
||||
pageBreaks && idx > 0 ? "page-break-before" : ""
|
||||
} pt-8 first:pt-0 print:pt-6`}
|
||||
} pt-6 first:pt-0`}
|
||||
>
|
||||
{/* Formal Chapter Header */}
|
||||
<header className="challenge-header-card border-b-2 border-slate-800 pb-3 mb-6">
|
||||
{/* Chapter index label */}
|
||||
<div className="flex items-center justify-between gap-3 text-xs font-mono text-slate-600 mb-1">
|
||||
<span className="font-bold tracking-wider uppercase text-slate-800">
|
||||
CHAPTER {chNum} · CHALLENGE {chNum} OF {filteredAndSortedDocs.length}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
{doc.slug}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Challenge Title */}
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-slate-950 tracking-tight leading-snug mb-3">
|
||||
{doc.title}
|
||||
{/* Challenge Title & Metadata */}
|
||||
<div className="mb-4 pb-2 border-b border-slate-200">
|
||||
<h2 className="text-2xl font-bold text-slate-950 tracking-tight leading-tight mb-1.5">
|
||||
{chNum}. {doc.title}
|
||||
</h2>
|
||||
|
||||
{/* Formal Metadata Line */}
|
||||
<div className="badge-container flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-mono text-slate-700 pt-1">
|
||||
<span>
|
||||
<strong className="text-slate-900">Category:</strong> {cat}
|
||||
</span>
|
||||
{pts > 0 && (
|
||||
<span>
|
||||
<strong className="text-slate-900">Points:</strong> {pts} pts
|
||||
</span>
|
||||
)}
|
||||
{diff && (
|
||||
<span>
|
||||
<strong className="text-slate-900">Difficulty:</strong> {diff}
|
||||
</span>
|
||||
)}
|
||||
{isSolved && (
|
||||
<span>
|
||||
<strong className="text-slate-900">Status:</strong> Solved
|
||||
</span>
|
||||
)}
|
||||
{doc.author && (
|
||||
<span>
|
||||
<strong className="text-slate-900">Author:</strong> @{doc.author}
|
||||
</span>
|
||||
)}
|
||||
{doc.updatedAt && (
|
||||
<span>
|
||||
<strong className="text-slate-900">Date:</strong>{" "}
|
||||
{new Date(doc.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
<div className="text-xs font-mono text-slate-600 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span><strong>Category:</strong> {cat}</span>
|
||||
{pts > 0 && <span><strong>Points:</strong> {pts} pts</span>}
|
||||
{diff && <span><strong>Difficulty:</strong> {diff}</span>}
|
||||
{doc.author && <span><strong>Author:</strong> @{doc.author}</span>}
|
||||
{doc.slug && <span><strong>Slug:</strong> {doc.slug}</span>}
|
||||
|
||||
{/* 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 (
|
||||
<span key={k}>
|
||||
<strong className="text-slate-900">{k}:</strong> {String(v)}
|
||||
<strong>{k}:</strong> {String(v)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
{/* Markdown Writeup Body */}
|
||||
<div className="prose-content mb-10 text-slate-900">
|
||||
{/* Markdown Writeup Content */}
|
||||
<div className="prose prose-slate max-w-none text-slate-900 mb-8">
|
||||
<Markdown source={doc.body} />
|
||||
</div>
|
||||
|
||||
{/* Chapter End Divider (screen only) */}
|
||||
<div className="print:hidden border-b border-slate-200 my-8" />
|
||||
{/* Divider between challenges (screen only) */}
|
||||
<hr className="print:hidden border-slate-200 my-8" />
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user