Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 56 additions & 0 deletions src/components/github-info.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { fetcher, useFallbackData } from "@/lib/fetcher";
import { GitFork, Star } from "lucide-react";
import useSWR from "swr";

const defaultFormatter = new Intl.NumberFormat("en", {
notation: "compact",
maximumFractionDigits: 1,
});

type RepoInfo = {
stargazers_count: number;
forks: number;
};

export function GithubInfo() {
const {
data: fallbackData,
hasFreshData,
setCachedData,
markCacheWindow,
} = useFallbackData<RepoInfo>("github_info");
const { data } = useSWR<RepoInfo>("https://api.github.com/repos/OrcaCD/orca-cd", fetcher, {
onSuccess: setCachedData,
onError: markCacheWindow,
fallbackData: fallbackData ?? undefined,
revalidateOnMount: !hasFreshData,
revalidateOnFocus: false,
revalidateOnReconnect: false,
shouldRetryOnError: false,
});

const repoData = data ?? fallbackData;

return (
<a
href={`https://github.com/OrcaCD/orca-cd`}
rel="noreferrer noopener"
target="_blank"
className="flex flex-row gap-1.5 p-2 rounded-lg text-sm text-fd-foreground/80 transition-colors hover:text-fd-accent-foreground hover:bg-fd-accent"
>
<p className="flex items-center gap-2 truncate">
<svg fill="currentColor" viewBox="0 0 24 24" className="size-3.5">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
OrcaCD/orca-cd
</p>
<div className="flex text-xs items-center gap-1 text-fd-muted-foreground">
<Star className="size-3" />
<span>{!repoData ? "..." : defaultFormatter.format(repoData.stargazers_count)}</span>
<GitFork className="size-3 ms-2" />
<span>{!repoData ? "..." : defaultFormatter.format(repoData.forks)}</span>
</div>
</a>
);
}
30 changes: 26 additions & 4 deletions src/components/github-release.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
import { fetcher, useFallbackData } from "@/lib/fetcher";
import useSWR from "swr";

type ReleaseData = {
tag_name: string;
};

export function GitHubRelease() {
const { data, isLoading } = useSWR(
const {
data: fallbackData,
hasFreshData,
setCachedData,
markCacheWindow,
} = useFallbackData<ReleaseData>("github_release");
const { data, isLoading } = useSWR<ReleaseData>(
"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()),
fetcher,
{
onSuccess: setCachedData,
onError: markCacheWindow,
fallbackData: fallbackData ?? undefined,
revalidateOnMount: !hasFreshData,
revalidateOnFocus: false,
revalidateOnReconnect: false,
shouldRetryOnError: false,
},
);

const releaseData = data ?? fallbackData;
const isPending = !releaseData && isLoading;

return (
<a
href="https://github.com/OrcaCD/orca-cd/releases"
target="_blank"
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")}
{isPending ? "..." : (releaseData?.tag_name ?? "No release yet")}
</a>
);
}
117 changes: 117 additions & 0 deletions src/lib/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useEffect, useState } from "react";

const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000;

type LocalStorageCache<JSON> = {
value: JSON | null;
expiresAt: number;
};

type FallbackDataState<JSON> = {
data: JSON | null;
hasFreshData: boolean;
};

function isLocalStorageCache<JSON>(value: unknown): value is LocalStorageCache<JSON> {
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<JSON>(key: string): FallbackDataState<JSON> {
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<JSON>(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<JSON = any>(input: RequestInfo, init?: RequestInit): Promise<JSON> {
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 useFallbackData<JSON = any>(key: string, ttlMs = DEFAULT_CACHE_TTL_MS) {
const [fallbackData, setFallbackData] = useState<FallbackDataState<JSON>>(() =>
readFallbackData<JSON>(key),
);

const setCacheEntry = (data: JSON | null) => {
if (typeof window === "undefined") {
return;
}

const cachePayload: LocalStorageCache<JSON> = {
value: data,
expiresAt: Date.now() + ttlMs,
};

localStorage.setItem(key, JSON.stringify(cachePayload));
setFallbackData({
data,
hasFreshData: true,
});
};

useEffect(() => {
setFallbackData(readFallbackData<JSON>(key));

const handleStorageChange = (event: StorageEvent) => {
if (event.key === key) {
setFallbackData(readFallbackData<JSON>(key));
}
};

window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange);
};
}, [key]);

return {
data: fallbackData.data,
hasFreshData: fallbackData.hasFreshData,
setCachedData: setCacheEntry,
markCacheWindow: () => setCacheEntry(fallbackData.data),
};
}
5 changes: 2 additions & 3 deletions src/lib/layout.shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -86,6 +85,6 @@ export const navbarLinks: LinkItemType[] = [
{
type: "custom",
secondary: true,
children: <GithubInfo owner="OrcaCD" repo="orca-cd" className="flex-row" />,
children: <GithubInfo />,
},
];