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.
This commit is contained in:
James Pine
2026-03-15 08:37:23 -07:00
parent a5269d23db
commit a637aebe69
2 changed files with 65 additions and 2 deletions
+10 -2
View File
@@ -13,6 +13,8 @@ import type { DownloadLinks } from '@/lib/releases';
export default function Home() {
const [downloadLinks, setDownloadLinks] = useState<DownloadLinks>(DOWNLOAD_LINKS);
const [version, setVersion] = useState<string | null>(null);
const [totalDownloads, setTotalDownloads] = useState<number | null>(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() {
</a>
</div>
{/* Version */}
{/* Version + downloads */}
<p
className="fade-in mt-4 text-xs text-muted-foreground/50"
style={{ animationDelay: '400ms' }}
>
Free and open source &middot; macOS, Windows, Linux
{version ?? ''}
{version && totalDownloads != null ? ' \u00b7 ' : ''}
{totalDownloads != null ? `${totalDownloads.toLocaleString()} downloads` : ''}
{version || totalDownloads != null ? ' \u00b7 ' : ''}
macOS, Windows, Linux
</p>
</div>
+55
View File
@@ -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<ReleaseInfo> {
}
}
// 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<ReleaseInfo> {
}
}
// 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<number> {
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
*/