Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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>
);
}
125 changes: 125 additions & 0 deletions src/lib/fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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,
});
};

const setCachedData = (data: JSON) => {
Comment thread
alex289 marked this conversation as resolved.
Outdated
setCacheEntry(data);
};

const markCacheWindow = () => {
setCacheEntry(fallbackData.data);
};

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,
markCacheWindow,
};
}
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 />,
},
];