From a637aebe695211d75e727ab808a4437dc00279b7 Mon Sep 17 00:00:00 2001 From: James Pine Date: Sun, 15 Mar 2026 08:37:23 -0700 Subject: [PATCH] feat: show version and total download count on landing page Fetches download counts across all GitHub releases (paginated) and displays version, total downloads, and platform list below the CTA. --- landing/src/app/page.tsx | 12 ++++++-- landing/src/lib/releases.ts | 55 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/landing/src/app/page.tsx b/landing/src/app/page.tsx index 203ddf1b..06ebe2e7 100644 --- a/landing/src/app/page.tsx +++ b/landing/src/app/page.tsx @@ -13,6 +13,8 @@ import type { DownloadLinks } from '@/lib/releases'; export default function Home() { const [downloadLinks, setDownloadLinks] = useState(DOWNLOAD_LINKS); + const [version, setVersion] = useState(null); + const [totalDownloads, setTotalDownloads] = useState(null); useEffect(() => { fetch('/api/releases') @@ -22,6 +24,8 @@ export default function Home() { }) .then((data) => { if (data.downloadLinks) setDownloadLinks(data.downloadLinks); + if (data.version) setVersion(data.version); + if (data.totalDownloads != null) setTotalDownloads(data.totalDownloads); }) .catch((error) => { console.error('Failed to fetch release info:', error); @@ -96,12 +100,16 @@ export default function Home() { - {/* Version */} + {/* Version + downloads */}

- Free and open source · macOS, Windows, Linux + {version ?? ''} + {version && totalDownloads != null ? ' \u00b7 ' : ''} + {totalDownloads != null ? `${totalDownloads.toLocaleString()} downloads` : ''} + {version || totalDownloads != null ? ' \u00b7 ' : ''} + macOS, Windows, Linux

diff --git a/landing/src/lib/releases.ts b/landing/src/lib/releases.ts index 3f985d31..b2847dfe 100644 --- a/landing/src/lib/releases.ts +++ b/landing/src/lib/releases.ts @@ -9,6 +9,7 @@ export interface DownloadLinks { export interface ReleaseInfo { version: string; downloadLinks: DownloadLinks; + totalDownloads: number; } const GITHUB_REPO = 'jamiepine/voicebox'; @@ -72,11 +73,15 @@ export async function getLatestRelease(): Promise { } } + // Fetch total downloads across ALL releases + const totalDownloads = await getTotalDownloads(); + // Fallback: construct URLs if not found in assets const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}`; const releaseInfo: ReleaseInfo = { version, + totalDownloads, downloadLinks: { macArm: downloadLinks.macArm || `${baseUrl}/voicebox_aarch64.app.tar.gz`, macIntel: downloadLinks.macIntel || `${baseUrl}/voicebox_x64.app.tar.gz`, @@ -97,6 +102,56 @@ export async function getLatestRelease(): Promise { } } +// Cache for total download count +let cachedTotalDownloads: number | null = null; +let downloadsCacheTimestamp: number = 0; + +/** + * Fetches download counts across ALL releases (paginated) + */ +async function getTotalDownloads(): Promise { + const now = Date.now(); + if (cachedTotalDownloads !== null && now - downloadsCacheTimestamp < CACHE_DURATION) { + return cachedTotalDownloads; + } + + let total = 0; + let page = 1; + + try { + while (true) { + const response = await fetch( + `${GITHUB_API_BASE}/repos/${GITHUB_REPO}/releases?per_page=100&page=${page}`, + { + headers: { Accept: 'application/vnd.github.v3+json' }, + }, + ); + + if (!response.ok) break; + + const releases = await response.json(); + if (!Array.isArray(releases) || releases.length === 0) break; + + for (const release of releases) { + for (const asset of release.assets || []) { + total += asset.download_count || 0; + } + } + + if (releases.length < 100) break; + page++; + } + + cachedTotalDownloads = total; + downloadsCacheTimestamp = now; + } catch (error) { + console.error('Failed to fetch total downloads:', error); + if (cachedTotalDownloads !== null) return cachedTotalDownloads; + } + + return total; +} + /** * Fetches the star count for the repo from GitHub */