mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
* feat(landing): show live GitHub star count on the header GitHub button Add a small client hook (useGithubStars) that fetches stargazers_count from the GitHub API and a formatStarCount helper that renders it in GitHub's compact repo-header style (e.g. "37.6k"). The landing header's GitHub button now appends a star badge (faint divider + filled star + count) on both the desktop and mobile menu entries. Fetched client-side on purpose: LandingHeader is shared across every marketing page, so one client fetch covers them all without threading a server value through each render site, and each visitor calls the API from their own IP, sidestepping the shared-outbound-IP rate limit the server-side github-release fetcher works around with a PAT. The result is memoized at module scope (plus in-flight dedupe); a failed fetch caches null and the button degrades to the plain "GitHub" label. * fix(landing): drop the star glyph from the GitHub star badge In the GitHub button context the number already reads as the star count, so the icon is redundant. Keep the divider + count only.
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
/**
|
|
* Live GitHub star count for the landing header's "GitHub" button.
|
|
*
|
|
* Fetched client-side on purpose: the badge lives in the shared
|
|
* {@link LandingHeader}, which renders on every marketing page, so a single
|
|
* client fetch covers them all without threading a server value through eight
|
|
* render sites. Each visitor calls the GitHub API from their own IP, which
|
|
* sidesteps the shared-outbound-IP rate limit that the server-side
|
|
* `github-release.ts` fetcher has to work around with a PAT.
|
|
*
|
|
* The result is memoized at module scope (plus an in-flight promise) so
|
|
* client-side navigation between landing pages reuses the first fetch instead
|
|
* of hitting the API again. A failed fetch caches `null` so we don't retry in
|
|
* a loop; the button just degrades to its plain "GitHub" label.
|
|
*/
|
|
|
|
const REPO = "multica-ai/multica";
|
|
|
|
// `undefined` = never fetched; `number` = resolved count; `null` = fetch failed.
|
|
let cachedStars: number | null | undefined;
|
|
let inFlight: Promise<number | null> | null = null;
|
|
|
|
async function loadStars(): Promise<number | null> {
|
|
if (cachedStars !== undefined) return cachedStars;
|
|
if (inFlight) return inFlight;
|
|
|
|
inFlight = fetch(`https://api.github.com/repos/${REPO}`, {
|
|
headers: { Accept: "application/vnd.github+json" },
|
|
})
|
|
.then((res) => {
|
|
if (!res.ok) throw new Error(`GitHub API responded ${res.status}`);
|
|
return res.json() as Promise<{ stargazers_count?: unknown }>;
|
|
})
|
|
.then((data) => {
|
|
const count =
|
|
typeof data.stargazers_count === "number" ? data.stargazers_count : null;
|
|
cachedStars = count;
|
|
return count;
|
|
})
|
|
.catch(() => {
|
|
cachedStars = null;
|
|
return null;
|
|
})
|
|
.finally(() => {
|
|
inFlight = null;
|
|
});
|
|
|
|
return inFlight;
|
|
}
|
|
|
|
export function useGithubStars(): number | null {
|
|
const [stars, setStars] = useState<number | null>(cachedStars ?? null);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
void loadStars().then((count) => {
|
|
if (active && count != null) setStars(count);
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, []);
|
|
|
|
return stars;
|
|
}
|
|
|
|
/**
|
|
* Compact star count matching GitHub's own repo-header style: one decimal
|
|
* thousands/millions with the trailing ".0" trimmed ("1k", "37.6k", "1.2m").
|
|
* Counts below 1,000 render exactly. Mirrors GitHub's `toFixed(1)` rounding so
|
|
* our badge reads the same as the figure on the repo page.
|
|
*/
|
|
export function formatStarCount(n: number): string {
|
|
if (n >= 1_000_000) {
|
|
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;
|
|
}
|
|
if (n >= 1_000) {
|
|
return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
}
|
|
return String(n);
|
|
}
|