diff --git a/src/routes/changelog.tsx b/src/routes/changelog.tsx index 73aacec..95b2dcc 100644 --- a/src/routes/changelog.tsx +++ b/src/routes/changelog.tsx @@ -8,6 +8,7 @@ import { Download, ExternalLink, GitCommit, + GithubIcon, Loader2, Plus, Tag, @@ -46,37 +47,92 @@ function parseReleaseBody(body: string): ParsedSection[] { const sections: ParsedSection[] = []; const normalized = body.replace(/\r\n/g, "\n"); - // Parse sections within the release body - const sectionRegex = - /### (Added|Changed|Fixed|Technical Details|Usage Examples|Known Limitations)\n([\s\S]*?)(?=###|$)/g; - const sectionMatches = [...normalized.matchAll(sectionRegex)]; + // Split by section headers (### Header) + const sectionSplit = normalized.split(/^### /m); - for (const sectionMatch of sectionMatches) { - const sectionTitle = sectionMatch[1]; - const sectionContent = sectionMatch[2].trim(); + // First element is before any header, skip it + for (let i = 1; i < sectionSplit.length; i++) { + const part = sectionSplit[i]; + const lines = part.split("\n"); + const firstLine = lines[0]; + + // Extract section title from first line + let sectionTitle = firstLine; + let sectionContent = lines.slice(1).join("\n").trim(); let type: ParsedSection["type"] = "other"; - if (sectionTitle === "Added") type = "added"; - else if (sectionTitle === "Changed") type = "changed"; - else if (sectionTitle === "Fixed") type = "fixed"; - else if (sectionTitle === "Technical Details") type = "technical"; + if (firstLine === "Added") { + type = "added"; + } else if (firstLine === "Changed") { + type = "changed"; + } else if (firstLine === "Fixed") { + type = "fixed"; + } else if (firstLine === "Technical Details") { + type = "technical"; + } - // Parse items - look for bullet points or sub-sections + // Parse items - look for bullet points, numbered items, or sub-sections const items: string[] = []; - const lines = sectionContent.split("\n"); + const contentLines = sectionContent.split("\n"); + let currentItem = ""; + let currentSubsection = ""; + + for (const line of contentLines) { + // Skip completely empty lines + if (!line.trim()) continue; - for (const line of lines) { - if (line.startsWith("- ") || line.startsWith("* ")) { + // Sub-section headers (#### ) + if (line.startsWith("#### ")) { if (currentItem) items.push(currentItem.trim()); - currentItem = line.slice(2); - } else if (line.startsWith("#### ")) { + currentSubsection = line.slice(5); // Remove "#### " + currentItem = ""; // Reset, don't create item yet + continue; + } + + // Top-level bullets (not indented) + if ( + (line.startsWith("- ") || line.startsWith("* ")) && + !line.startsWith(" ") + ) { + if (currentItem) items.push(currentItem.trim()); + const bulletText = line.slice(2); + // If we have a subsection, include it with the first item + if (currentSubsection && currentItem === "") { + currentItem = `**${currentSubsection}**\n${bulletText}`; + currentSubsection = ""; // Consumed + } else { + currentItem = bulletText; + } + } + // Numbered items (1., 2., etc.) not indented + else if (/^\d+\.\s/.test(line) && !line.startsWith(" ")) { if (currentItem) items.push(currentItem.trim()); - currentItem = `**${line.slice(5)}**`; - } else if (line.trim() && currentItem) { - currentItem += ` ${line.trim()}`; + const numberedText = line.replace(/^\d+\.\s/, ""); + // If we have a subsection, include it with the first item + if (currentSubsection && currentItem === "") { + currentItem = `**${currentSubsection}**\n${numberedText}`; + currentSubsection = ""; // Consumed + } else { + currentItem = numberedText; + } + } + // Sub-items (indented bullets or numbered) + else if ( + line.startsWith(" - ") || + line.startsWith(" * ") || + /^ \d+\.\s/.test(line) + ) { + if (currentItem) { + currentItem += `\n${line.trim()}`; + } + } + // Continuation lines (non-empty, not starting with special chars) + else if (line.trim() && currentItem) { + currentItem += `\n${line.trim()}`; } } + if (currentItem) items.push(currentItem.trim()); if (items.length > 0) { @@ -116,7 +172,7 @@ function getSectionColor(type: string) { } function ReleaseSection({ section }: { section: ParsedSection }) { - const [expanded, setExpanded] = useState(section.type === "added"); + const [expanded, setExpanded] = useState(false); return (
@@ -148,20 +204,25 @@ function ReleaseSection({ section }: { section: ParsedSection }) { {section.items.map((item) => (
- $1', - ) - .replace( - /`(.*?)`/g, - '$1', - ), - }} - /> +
+ {item.split("\n").map((line, idx) => ( +
0 ? "ml-2 text-white/60" : ""} + dangerouslySetInnerHTML={{ + __html: line + .replace( + /\*\*(.*?)\*\*/g, + '$1', + ) + .replace( + /`(.*?)`/g, + '$1', + ), + }} + /> + ))} +
))}
@@ -223,7 +284,7 @@ function ReleaseCard({ Latest )} - {release.prerelease && ( + {release.prerelease && ( Pre-release @@ -315,6 +376,13 @@ function ChangelogPage() { const [releases, setReleases] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const itemsPerPage = 10; + const totalPages = Math.ceil(releases.length / itemsPerPage); + const paginatedReleases = releases.slice( + (page - 1) * itemsPerPage, + page * itemsPerPage, + ); useEffect(() => { async function fetchReleases() { @@ -379,8 +447,7 @@ function ChangelogPage() { rel="noopener noreferrer" className="text-sm text-white/50 hover:text-white transition-colors flex items-center gap-1" > - GitHub - +
@@ -425,7 +492,7 @@ function ChangelogPage() { releases
-
+
Latest:{" "} {releases[0]?.tag_name} @@ -465,19 +532,59 @@ function ChangelogPage() { )} {!loading && !error && releases.length > 0 && ( + <>
- {releases.map((release, idx) => ( + {paginatedReleases.map((release, idx) => ( ))}
+ + {/* Pagination */} + {totalPages > 1 && ( +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map((p) => ( + + ))} + + +
+ )} + )} - {/* End of timeline */} - {!loading && !error && releases.length > 0 && ( + {/* End of timeline - only show on last page */} + {!loading && !error && releases.length > 0 && page === totalPages && (