94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import styles from "./GitHubStarButton.module.css";
|
|
|
|
interface Props {
|
|
repo: string;
|
|
// SSR-fed initial count from Nav's server-side fetch. When present
|
|
// the badge renders the number on first paint; the client-side
|
|
// refetch below is a best-effort refresh that only fires if it
|
|
// succeeds (unauthenticated github.com api is rate-limited to
|
|
// ~60/hr/IP, which is why the count was invisible before).
|
|
initialStars?: number;
|
|
}
|
|
|
|
function formatStars(n: number): string {
|
|
if (n >= 1000) {
|
|
const k = n / 1000;
|
|
return k >= 10 ? `${Math.round(k)}k` : `${k.toFixed(1).replace(/\.0$/, "")}k`;
|
|
}
|
|
return String(n);
|
|
}
|
|
|
|
export function GitHubStarButton({ repo, initialStars }: Props) {
|
|
const [stars, setStars] = useState<number | null>(
|
|
typeof initialStars === "number" ? initialStars : null,
|
|
);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const cacheKey = `gh-stars:${repo}`;
|
|
const cached = typeof window !== "undefined" ? window.localStorage.getItem(cacheKey) : null;
|
|
if (cached) {
|
|
try {
|
|
const parsed = JSON.parse(cached) as { value: number; at: number };
|
|
if (Date.now() - parsed.at < 30 * 60 * 1000) {
|
|
setStars(parsed.value);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
(async () => {
|
|
try {
|
|
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
headers: { Accept: "application/vnd.github+json" },
|
|
});
|
|
if (!res.ok) return;
|
|
const data = (await res.json()) as { stargazers_count?: number };
|
|
if (cancelled) return;
|
|
if (typeof data.stargazers_count === "number") {
|
|
setStars(data.stargazers_count);
|
|
try {
|
|
window.localStorage.setItem(
|
|
cacheKey,
|
|
JSON.stringify({ value: data.stargazers_count, at: Date.now() }),
|
|
);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
} catch {
|
|
/* offline / blocked — keep SSR / cached / null */
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [repo]);
|
|
|
|
return (
|
|
<a
|
|
href={`https://github.com/${repo}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`btn btn--ghost ${styles.starBtn}`}
|
|
aria-label={`Star ${repo} on GitHub${stars !== null ? ` — ${stars.toLocaleString()} stars` : ""}`}
|
|
>
|
|
<svg
|
|
aria-hidden
|
|
viewBox="0 0 16 16"
|
|
width="18"
|
|
height="18"
|
|
className={styles.icon}
|
|
fill="currentColor"
|
|
>
|
|
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
|
|
</svg>
|
|
<span className={styles.label}>STAR</span>
|
|
{stars !== null && <span className={styles.count}>{formatStars(stars)}</span>}
|
|
</a>
|
|
);
|
|
}
|