diff --git a/website/src/components/Header.tsx b/website/src/components/Header.tsx index 95a3ae0bbfe..5f0436e1cbc 100644 --- a/website/src/components/Header.tsx +++ b/website/src/components/Header.tsx @@ -6,17 +6,11 @@ import { getLatestBlogPost } from "@/src/helpers/blogPosts"; import { getGitHubStarCount } from "@/src/helpers/githubStars"; import { ChilliCreamText } from "@/src/icons/ChilliCreamText"; import { ChilliCreamWinking } from "@/src/icons/ChilliCreamWinking"; -import { GitHubIcon } from "@/src/icons/GitHub"; import { HeaderShell } from "./HeaderShell"; +import { GitHubStarButton } from "./header/GitHubStarButton"; import { HeaderNav } from "./header/HeaderNav"; -import { - CONTACT_HREF, - GITHUB_REPO_URL, - GITHUB_STARGAZERS_URL, - MOBILE_ITEMS, - TOOLS, -} from "./header/navData"; +import { CONTACT_HREF, MOBILE_ITEMS, TOOLS } from "./header/navData"; import { MobileNav } from "./MobileNav"; import { Search } from "./Search"; @@ -51,29 +45,7 @@ export default async function Header() {
- - - - Star - - {starCount !== null && ( - - {starCount.toLocaleString("en-US")} - - )} - + ; + + return typeof cached.count === "number" && + typeof cached.fetchedAt === "number" && + Date.now() - cached.fetchedAt < CACHE_MAX_AGE_MS + ? cached.count + : null; + } catch { + return null; + } +} + +function writeCachedCount(count: number): void { + try { + const cached: CachedStarCount = { count, fetchedAt: Date.now() }; + + window.sessionStorage.setItem(CACHE_KEY, JSON.stringify(cached)); + } catch { + // Session storage can be unavailable; the count is simply fetched again on + // the next page load. + } +} + +async function fetchStarCount(signal: AbortSignal): Promise { + try { + const response = await fetch(GITHUB_REPO_API, { + headers: { Accept: "application/vnd.github+json" }, + signal, + }); + + if (!response.ok) { + return null; + } + + const data = (await response.json()) as { stargazers_count?: number }; + + return typeof data.stargazers_count === "number" + ? data.stargazers_count + : null; + } catch { + return null; + } +} + +async function resolveStarCount(signal: AbortSignal): Promise { + const cached = readCachedCount(); + + if (cached !== null) { + return cached; + } + + const liveCount = await fetchStarCount(signal); + + if (liveCount !== null) { + writeCachedCount(liveCount); + } + + return liveCount; +} + +interface GitHubStarButtonProps { + readonly initialCount: number | null; +} + +export function GitHubStarButton({ initialCount }: GitHubStarButtonProps) { + const [count, setCount] = useState(initialCount); + + useEffect(() => { + const controller = new AbortController(); + + void resolveStarCount(controller.signal).then((resolved) => { + if (resolved !== null && !controller.signal.aborted) { + setCount(resolved); + } + }); + + return () => controller.abort(); + }, []); + + return ( + + + + Star + + {count !== null && ( + + {count.toLocaleString("en-US")} + + )} + + ); +}