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")}
+
+ )}
+
+ );
+}