From 8b64fc81e1f87f72d6a82539f1f6c96893e25f17 Mon Sep 17 00:00:00 2001 From: Paul Ehikhuemen Date: Mon, 12 Oct 2026 10:22:34 -0400 Subject: [PATCH] feat(web): show release versions in changelog timeline --- apps/web/src/ui/components/Changelog.astro | 100 +++++++++++++++------ 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/apps/web/src/ui/components/Changelog.astro b/apps/web/src/ui/components/Changelog.astro index 69ea5425..46c29956 100644 --- a/apps/web/src/ui/components/Changelog.astro +++ b/apps/web/src/ui/components/Changelog.astro @@ -1,4 +1,6 @@ --- +import { execFileSync } from 'node:child_process'; + // Define types for our data structures interface Author { img: string; @@ -17,8 +19,18 @@ interface PREntry { pr: PullRequest; } +interface ReleaseEntry { + name: string; + url: string; +} + +interface TimelineDay { + contributions: PREntry[]; + releases: ReleaseEntry[]; +} + interface ProcessedData { - [date: string]: PREntry[]; + [date: string]: TimelineDay; } interface GitHubPR { @@ -33,11 +45,15 @@ interface GitHubPR { }; } -type TimelineEntry = [string, PREntry[]]; +type TimelineEntry = [string, TimelineDay]; -// Fetch PR data from GitHub API -let sortedData: TimelineEntry[] = []; +const processedData: ProcessedData = {}; +const getTimelineDay = (date: string) => { + processedData[date] ??= { contributions: [], releases: [] }; + return processedData[date]; +}; +// Fetch PR data from GitHub API try { // Use native fetch instead of Octokit const response = await fetch( @@ -53,21 +69,14 @@ try { throw new Error(`GitHub API responded with ${response.status}: ${response.statusText}`); } - const releases = (await response.json()) as GitHubPR[]; + const pullRequests = (await response.json()) as GitHubPR[]; - // Process the data similar to the Vue implementation - const processedData: ProcessedData = {}; - - releases.forEach((pr) => { + pullRequests.forEach((pr) => { if (!pr.merged_at) return; const day = new Date(pr.merged_at).toISOString().split('T')[0]; - if (!processedData[day]) { - processedData[day] = []; - } - - processedData[day].push({ + getTimelineDay(day).contributions.push({ author: { img: pr.user.avatar_url, name: pr.user.login, @@ -80,17 +89,45 @@ try { } }); }); +} catch (error) { + console.error('Error fetching GitHub data:', error); +} - // Sort by date (newest first) - const sortedDates = Object.keys(processedData).sort( - (a, b) => new Date(b).getTime() - new Date(a).getTime() +// Read recent release tags from the repository used to build the site +try { + const releaseTags = execFileSync( + 'git', + [ + 'for-each-ref', + '--sort=-creatordate', + '--count=5', + '--format=%(refname:short)|%(creatordate:short)', + 'refs/tags/jsx-email-v*' + ], + { encoding: 'utf8' } ); - sortedData = sortedDates.map((date) => [date, processedData[date]]); + + releaseTags + .trim() + .split('\n') + .filter(Boolean) + .forEach((releaseTag) => { + const [name, day] = releaseTag.split('|'); + + getTimelineDay(day).releases.push({ + name: name.replace('jsx-email-', ''), + url: `https://github.com/shellscape/jsx-email/tree/${name}` + }); + }); } catch (error) { - console.error('Error fetching GitHub data:', error); - // Provide fallback data if API fails - sortedData = []; + console.error('Error reading release tags:', error); } + +// Sort by date (newest first) +const sortedDates = Object.keys(processedData).sort( + (a, b) => new Date(b).getTime() - new Date(a).getTime() +); +const sortedData: TimelineEntry[] = sortedDates.map((date) => [date, processedData[date]]); ---
@@ -120,7 +157,7 @@ try {
{ sortedData.length > 0 ? ( - sortedData.map(([day, events], index) => ( + sortedData.map(([day, { contributions, releases }], index) => (
- {/* */} - {events.map((entry: PREntry) => ( + {/* */} + {releases.map((release) => ( + + Release + {release.name} + + ))} + + {/* */} + {contributions.map((entry: PREntry) => (