From df767827fff585fcd5158be654555bca2fab4c13 Mon Sep 17 00:00:00 2001 From: alex289 Date: Tue, 5 May 2026 15:21:51 +0200 Subject: [PATCH 1/5] fix: Customize github info to fix rate limiting --- package-lock.json | 7 +++--- package.json | 1 + src/components/github-info.tsx | 38 +++++++++++++++++++++++++++++++ src/components/github-release.tsx | 4 ++-- src/lib/layout.shared.tsx | 5 ++-- 5 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 src/components/github-info.tsx diff --git a/package-lock.json b/package-lock.json index e1f81c4..31a7bfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "fumadocs-core": "16.8.5", "fumadocs-mdx": "14.3.2", "fumadocs-ui": "16.8.5", + "lucide-react": "^1.14.0", "lucide-static": "^1.14.0", "react": "^19.2.5", "react-dom": "^19.2.5", @@ -5765,9 +5766,9 @@ } }, "node_modules/lucide-react": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", - "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index 95df64c..aff8f15 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "fumadocs-core": "16.8.5", "fumadocs-mdx": "14.3.2", "fumadocs-ui": "16.8.5", + "lucide-react": "^1.14.0", "lucide-static": "^1.14.0", "react": "^19.2.5", "react-dom": "^19.2.5", diff --git a/src/components/github-info.tsx b/src/components/github-info.tsx new file mode 100644 index 0000000..703451b --- /dev/null +++ b/src/components/github-info.tsx @@ -0,0 +1,38 @@ +import { GitFork, Star } from "lucide-react"; +import useSWR from "swr"; + +const defaultFormatter = new Intl.NumberFormat("en", { + notation: "compact", + maximumFractionDigits: 1, +}); + +export function GithubInfo() { + const { data } = useSWR( + "https://api.github.com/repos/OrcaCD/orca-cd", + // oxlint-disable-next-line promise/prefer-await-to-then + (...args) => fetch(...args).then((res) => res.json()), + ); + + return ( + +

+ + GitHub + + + OrcaCD/orca-cd +

+
+ + {!data ? "..." : defaultFormatter.format(data.stargazers_count)} + + {!data ? "..." : defaultFormatter.format(data.forks)} +
+
+ ); +} diff --git a/src/components/github-release.tsx b/src/components/github-release.tsx index 04c6eeb..e54312f 100644 --- a/src/components/github-release.tsx +++ b/src/components/github-release.tsx @@ -1,7 +1,7 @@ import useSWR from "swr"; export function GitHubRelease() { - const { data, isLoading } = useSWR( + const { data } = useSWR( "https://api.github.com/repos/OrcaCD/orca-cd/releases/latest", // oxlint-disable-next-line promise/prefer-await-to-then (...args) => fetch(...args).then((res) => res.json()), @@ -14,7 +14,7 @@ export function GitHubRelease() { rel="noopener noreferrer" className="inline-flex items-center rounded-md border border-fd-border bg-fd-card px-1 py-0.5 text-sm text-fd-muted-foreground transition-colors hover:bg-fd-accent" > - {isLoading ? "..." : (data.tag_name ?? "No release yet")} + {!data ? "..." : (data.tag_name ?? "No release yet")} ); } diff --git a/src/lib/layout.shared.tsx b/src/lib/layout.shared.tsx index b8edb24..2115bcf 100644 --- a/src/lib/layout.shared.tsx +++ b/src/lib/layout.shared.tsx @@ -5,10 +5,9 @@ import { NavbarMenuLink, NavbarMenuTrigger, } from "fumadocs-ui/layouts/home/navbar"; -import { GithubInfo } from "fumadocs-ui/components/github-info"; - import type { BaseLayoutProps, LinkItemType } from "fumadocs-ui/layouts/shared"; import { GitHubRelease } from "@/components/github-release"; +import { GithubInfo } from "@/components/github-info"; export function baseOptions(): BaseLayoutProps { return { @@ -86,6 +85,6 @@ export const navbarLinks: LinkItemType[] = [ { type: "custom", secondary: true, - children: , + children: , }, ]; From 0e797fc3965cb80c3fa9249127441f9ba5c54ac4 Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 6 May 2026 12:01:23 +0200 Subject: [PATCH 2/5] feat: Store fetched data in localstorage --- src/components/github-info.tsx | 15 ++++++++++----- src/components/github-release.tsx | 16 ++++++++++------ src/lib/fetcher.ts | 15 +++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/lib/fetcher.ts diff --git a/src/components/github-info.tsx b/src/components/github-info.tsx index 703451b..0f29ae6 100644 --- a/src/components/github-info.tsx +++ b/src/components/github-info.tsx @@ -1,3 +1,4 @@ +import { fetcher, getFallbackData } from "@/lib/fetcher"; import { GitFork, Star } from "lucide-react"; import useSWR from "swr"; @@ -7,11 +8,15 @@ const defaultFormatter = new Intl.NumberFormat("en", { }); export function GithubInfo() { - const { data } = useSWR( - "https://api.github.com/repos/OrcaCD/orca-cd", - // oxlint-disable-next-line promise/prefer-await-to-then - (...args) => fetch(...args).then((res) => res.json()), - ); + const { data } = useSWR<{ + stargazers_count: number; + forks: number; + }>("https://api.github.com/repos/OrcaCD/orca-cd", fetcher, { + onSuccess: (data) => { + localStorage.setItem("github_info", JSON.stringify(data)); + }, + fallbackData: getFallbackData("github_info"), + }); return ( fetch(...args).then((res) => res.json()), - ); + const { data, isLoading } = useSWR<{ + tag_name: string; + }>("https://api.github.com/repos/OrcaCD/orca-cd/releases/latest", fetcher, { + onSuccess: (data) => { + localStorage.setItem("github_release", JSON.stringify(data)); + }, + fallbackData: getFallbackData("github_release"), + }); return ( - {!data ? "..." : (data.tag_name ?? "No release yet")} + {isLoading ? "..." : (data?.tag_name ?? "No release yet")} ); } diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts new file mode 100644 index 0000000..db4e4da --- /dev/null +++ b/src/lib/fetcher.ts @@ -0,0 +1,15 @@ +export async function fetcher(input: RequestInfo, init?: RequestInit): Promise { + const res = await fetch(input, init); + if (!res.ok) { + throw new Error(`An error occurred while fetching the data: ${res.statusText}`); + } + return (await res.json()) as JSON; +} + +export function getFallbackData(key: string) { + const cached = localStorage.getItem(key); + if (cached) { + return JSON.parse(cached); + } + return null; +} From 736cc7b5890c719c5cdc232ffad92712ad85d67d Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 6 May 2026 12:09:24 +0200 Subject: [PATCH 3/5] fix: local storage not defined --- src/components/github-info.tsx | 15 ++++++++------ src/components/github-release.tsx | 23 +++++++++++++-------- src/lib/fetcher.ts | 34 +++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/components/github-info.tsx b/src/components/github-info.tsx index 0f29ae6..4de44b6 100644 --- a/src/components/github-info.tsx +++ b/src/components/github-info.tsx @@ -1,4 +1,4 @@ -import { fetcher, getFallbackData } from "@/lib/fetcher"; +import { fetcher, useFallbackData } from "@/lib/fetcher"; import { GitFork, Star } from "lucide-react"; import useSWR from "swr"; @@ -7,15 +7,18 @@ const defaultFormatter = new Intl.NumberFormat("en", { maximumFractionDigits: 1, }); +type RepoInfo = { + stargazers_count: number; + forks: number; +}; + export function GithubInfo() { - const { data } = useSWR<{ - stargazers_count: number; - forks: number; - }>("https://api.github.com/repos/OrcaCD/orca-cd", fetcher, { + const fallbackData = useFallbackData("github_info"); + const { data } = useSWR("https://api.github.com/repos/OrcaCD/orca-cd", fetcher, { onSuccess: (data) => { localStorage.setItem("github_info", JSON.stringify(data)); }, - fallbackData: getFallbackData("github_info"), + fallbackData: fallbackData ?? undefined, }); return ( diff --git a/src/components/github-release.tsx b/src/components/github-release.tsx index fab69fe..c6110f8 100644 --- a/src/components/github-release.tsx +++ b/src/components/github-release.tsx @@ -1,15 +1,22 @@ -import { fetcher, getFallbackData } from "@/lib/fetcher"; +import { fetcher, useFallbackData } from "@/lib/fetcher"; import useSWR from "swr"; +type ReleaseData = { + tag_name: string; +}; + export function GitHubRelease() { - const { data, isLoading } = useSWR<{ - tag_name: string; - }>("https://api.github.com/repos/OrcaCD/orca-cd/releases/latest", fetcher, { - onSuccess: (data) => { - localStorage.setItem("github_release", JSON.stringify(data)); + const fallbackData = useFallbackData("github_release"); + const { data, isLoading } = useSWR( + "https://api.github.com/repos/OrcaCD/orca-cd/releases/latest", + fetcher, + { + onSuccess: (data) => { + localStorage.setItem("github_release", JSON.stringify(data)); + }, + fallbackData: fallbackData ?? undefined, }, - fallbackData: getFallbackData("github_release"), - }); + ); return ( (input: RequestInfo, init?: RequestInit): Promise { const res = await fetch(input, init); if (!res.ok) { @@ -6,10 +8,30 @@ export async function fetcher(input: RequestInfo, init?: RequestInit return (await res.json()) as JSON; } -export function getFallbackData(key: string) { - const cached = localStorage.getItem(key); - if (cached) { - return JSON.parse(cached); - } - return null; +export function useFallbackData(key: string) { + const [fallbackData, setFallbackData] = useState(null); + + useEffect(() => { + const cached = localStorage.getItem(key); + if (cached) { + setFallbackData(JSON.parse(cached)); + } + + const handleStorageChange = (event: StorageEvent) => { + if (event.key === key) { + if (event.newValue) { + setFallbackData(JSON.parse(event.newValue)); + } else { + setFallbackData(null); + } + } + }; + + window.addEventListener("storage", handleStorageChange); + return () => { + window.removeEventListener("storage", handleStorageChange); + }; + }, [key]); + + return fallbackData; } From 899b9332b9a493c7c816e503d127857967b778d0 Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 6 May 2026 14:04:32 +0200 Subject: [PATCH 4/5] feat: Store cache for 1h --- src/components/github-info.tsx | 22 ++++-- src/components/github-release.tsx | 21 ++++-- src/lib/fetcher.ts | 112 ++++++++++++++++++++++++++---- 3 files changed, 132 insertions(+), 23 deletions(-) diff --git a/src/components/github-info.tsx b/src/components/github-info.tsx index 4de44b6..728c328 100644 --- a/src/components/github-info.tsx +++ b/src/components/github-info.tsx @@ -13,14 +13,24 @@ type RepoInfo = { }; export function GithubInfo() { - const fallbackData = useFallbackData("github_info"); + const { + data: fallbackData, + hasFreshData, + setCachedData, + markCacheWindow, + } = useFallbackData("github_info"); const { data } = useSWR("https://api.github.com/repos/OrcaCD/orca-cd", fetcher, { - onSuccess: (data) => { - localStorage.setItem("github_info", JSON.stringify(data)); - }, + onSuccess: setCachedData, + onError: markCacheWindow, fallbackData: fallbackData ?? undefined, + revalidateOnMount: !hasFreshData, + revalidateOnFocus: false, + revalidateOnReconnect: false, + shouldRetryOnError: false, }); + const repoData = data ?? fallbackData; + return (
- {!data ? "..." : defaultFormatter.format(data.stargazers_count)} + {!repoData ? "..." : defaultFormatter.format(repoData.stargazers_count)} - {!data ? "..." : defaultFormatter.format(data.forks)} + {!repoData ? "..." : defaultFormatter.format(repoData.forks)}
); diff --git a/src/components/github-release.tsx b/src/components/github-release.tsx index c6110f8..34904d0 100644 --- a/src/components/github-release.tsx +++ b/src/components/github-release.tsx @@ -6,18 +6,29 @@ type ReleaseData = { }; export function GitHubRelease() { - const fallbackData = useFallbackData("github_release"); + const { + data: fallbackData, + hasFreshData, + setCachedData, + markCacheWindow, + } = useFallbackData("github_release"); const { data, isLoading } = useSWR( "https://api.github.com/repos/OrcaCD/orca-cd/releases/latest", fetcher, { - onSuccess: (data) => { - localStorage.setItem("github_release", JSON.stringify(data)); - }, + onSuccess: setCachedData, + onError: markCacheWindow, fallbackData: fallbackData ?? undefined, + revalidateOnMount: !hasFreshData, + revalidateOnFocus: false, + revalidateOnReconnect: false, + shouldRetryOnError: false, }, ); + const releaseData = data ?? fallbackData; + const isPending = !releaseData && isLoading; + return ( - {isLoading ? "..." : (data?.tag_name ?? "No release yet")} + {isPending ? "..." : (releaseData?.tag_name ?? "No release yet")} ); } diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index d7eca7c..59e6c0a 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -1,5 +1,68 @@ import { useEffect, useState } from "react"; +const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; + +type LocalStorageCache = { + value: JSON | null; + expiresAt: number; +}; + +type FallbackDataState = { + data: JSON | null; + hasFreshData: boolean; +}; + +function isLocalStorageCache(value: unknown): value is LocalStorageCache { + if (!value || typeof value !== "object") { + return false; + } + + if (!("value" in value) || !("expiresAt" in value)) { + return false; + } + + return typeof (value as { expiresAt: unknown }).expiresAt === "number"; +} + +function readFallbackData(key: string): FallbackDataState { + if (typeof window === "undefined") { + return { + data: null, + hasFreshData: false, + }; + } + + const cached = localStorage.getItem(key); + if (!cached) { + return { + data: null, + hasFreshData: false, + }; + } + + try { + const parsed = JSON.parse(cached) as unknown; + + if (isLocalStorageCache(parsed)) { + return { + data: parsed.value, + hasFreshData: parsed.expiresAt > Date.now(), + }; + } + + // Backward compatibility for old cache entries without metadata. + return { + data: parsed as JSON, + hasFreshData: false, + }; + } catch { + return { + data: null, + hasFreshData: false, + }; + } +} + export async function fetcher(input: RequestInfo, init?: RequestInit): Promise { const res = await fetch(input, init); if (!res.ok) { @@ -8,22 +71,42 @@ export async function fetcher(input: RequestInfo, init?: RequestInit return (await res.json()) as JSON; } -export function useFallbackData(key: string) { - const [fallbackData, setFallbackData] = useState(null); +export function useFallbackData(key: string, ttlMs = DEFAULT_CACHE_TTL_MS) { + const [fallbackData, setFallbackData] = useState>(() => + readFallbackData(key), + ); - useEffect(() => { - const cached = localStorage.getItem(key); - if (cached) { - setFallbackData(JSON.parse(cached)); + const setCacheEntry = (data: JSON | null) => { + if (typeof window === "undefined") { + return; } + const cachePayload: LocalStorageCache = { + value: data, + expiresAt: Date.now() + ttlMs, + }; + + localStorage.setItem(key, JSON.stringify(cachePayload)); + setFallbackData({ + data, + hasFreshData: true, + }); + }; + + const setCachedData = (data: JSON) => { + setCacheEntry(data); + }; + + const markCacheWindow = () => { + setCacheEntry(fallbackData.data); + }; + + useEffect(() => { + setFallbackData(readFallbackData(key)); + const handleStorageChange = (event: StorageEvent) => { if (event.key === key) { - if (event.newValue) { - setFallbackData(JSON.parse(event.newValue)); - } else { - setFallbackData(null); - } + setFallbackData(readFallbackData(key)); } }; @@ -33,5 +116,10 @@ export function useFallbackData(key: string) { }; }, [key]); - return fallbackData; + return { + data: fallbackData.data, + hasFreshData: fallbackData.hasFreshData, + setCachedData, + markCacheWindow, + }; } From cc33ba6f07036ef68d8d57b6d17c87f1be7925be Mon Sep 17 00:00:00 2001 From: alex289 Date: Wed, 6 May 2026 14:40:42 +0200 Subject: [PATCH 5/5] feat: Apply review suggestions --- src/lib/fetcher.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/lib/fetcher.ts b/src/lib/fetcher.ts index 59e6c0a..c2b9848 100644 --- a/src/lib/fetcher.ts +++ b/src/lib/fetcher.ts @@ -93,14 +93,6 @@ export function useFallbackData(key: string, ttlMs = DEFAULT_CACHE_T }); }; - const setCachedData = (data: JSON) => { - setCacheEntry(data); - }; - - const markCacheWindow = () => { - setCacheEntry(fallbackData.data); - }; - useEffect(() => { setFallbackData(readFallbackData(key)); @@ -119,7 +111,7 @@ export function useFallbackData(key: string, ttlMs = DEFAULT_CACHE_T return { data: fallbackData.data, hasFreshData: fallbackData.hasFreshData, - setCachedData, - markCacheWindow, + setCachedData: setCacheEntry, + markCacheWindow: () => setCacheEntry(fallbackData.data), }; }