From 54a5a5d67020bb44a321d86aff20b76b6a818827 Mon Sep 17 00:00:00 2001 From: anurag629 Date: Wed, 8 Jul 2026 14:24:42 +0530 Subject: [PATCH] feat: on-site GitHub link with live star count Add a "Star on GitHub" control to the header (compact) and footer (labelled), so the open-source repo is one click from every page. - components/shared/GitHubStars.tsx: an async server component that fetches the repo's star count from the GitHub API with revalidate 3600 (hit at most once an hour, well under the unauthenticated limit) plus a 3s timeout. Any failure degrades to just the link, so a GitHub outage never breaks a page, and the count never reaches the browser. - Inlined GitHub mark SVG (lucide's brand icon is deprecated); amber Star icon. - lib/tools.ts: GITHUB_REPO / GITHUB_REPO_URL constants. The aria-label mirrors the visible abbreviated count and pluralizes; formatStars is consistent across the 1k/10k boundaries. --- components/shared/Footer.tsx | 19 +++++--- components/shared/GitHubStars.tsx | 77 +++++++++++++++++++++++++++++++ components/shared/Header.tsx | 2 + lib/tools.ts | 4 ++ 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 components/shared/GitHubStars.tsx diff --git a/components/shared/Footer.tsx b/components/shared/Footer.tsx index 9dc18f9..5f77786 100644 --- a/components/shared/Footer.tsx +++ b/components/shared/Footer.tsx @@ -1,16 +1,21 @@ +import { GitHubStars } from "./GitHubStars"; + export function Footer() { return ( ); diff --git a/components/shared/GitHubStars.tsx b/components/shared/GitHubStars.tsx new file mode 100644 index 0000000..78c920e --- /dev/null +++ b/components/shared/GitHubStars.tsx @@ -0,0 +1,77 @@ +import { Star } from "lucide-react"; +import { cn } from "@/lib/cn"; +import { GITHUB_REPO, GITHUB_REPO_URL } from "@/lib/tools"; + +// Server-side star-count fetch. Cached for an hour, so GitHub's API is hit at +// most once per hour (far under the 60/hr unauthenticated limit) no matter how +// much traffic we get, and no request ever reaches the browser. A bounded +// timeout plus a null fallback means a slow or failing GitHub never breaks a +// page — we just render the button without a count. +async function getStarCount(): Promise { + try { + const res = await fetch(`https://api.github.com/repos/${GITHUB_REPO}`, { + headers: { Accept: "application/vnd.github+json", "User-Agent": "toolbelt-site" }, + next: { revalidate: 3600 }, + signal: AbortSignal.timeout(3000), + }); + if (!res.ok) return null; + const data = (await res.json()) as { stargazers_count?: number }; + return typeof data.stargazers_count === "number" ? data.stargazers_count : null; + } catch { + return null; + } +} + +function formatStars(n: number): string { + if (n < 1000) return String(n); + const k = n / 1000; + // Decide the branch on the rounded value so 9.95k–9.999k renders "10k", + // not "10.0k" (which matches neither the sub-10k nor the 10k+ format). + return k >= 9.95 ? `${Math.round(k)}k` : `${k.toFixed(1)}k`; +} + +// The official GitHub mark, inlined so we don't depend on lucide's deprecated +// brand icons. +function GitHubMark({ className }: { className?: string }) { + return ( + + ); +} + +/** + * A "Star on GitHub" link with a live star count. Renders in both the header + * (compact, no label) and the footer (with label). If the count can't be + * fetched it degrades to just the link, so people can still star. + */ +export async function GitHubStars({ + showLabel = false, + className, +}: { + showLabel?: boolean; + className?: string; +}) { + const count = await getStarCount(); + return ( + + + {showLabel && Star on GitHub} + {count != null && ( + + + )} + + ); +} diff --git a/components/shared/Header.tsx b/components/shared/Header.tsx index 2513c17..b7c042e 100644 --- a/components/shared/Header.tsx +++ b/components/shared/Header.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { ArrowLeft } from "lucide-react"; import { ThemeToggle } from "./ThemeToggle"; import { CommandPaletteButton } from "./CommandPaletteButton"; +import { GitHubStars } from "./GitHubStars"; export function Header() { return ( @@ -37,6 +38,7 @@ export function Header() { Back to CODERCOPS + diff --git a/lib/tools.ts b/lib/tools.ts index 84b8437..79685b7 100644 --- a/lib/tools.ts +++ b/lib/tools.ts @@ -247,3 +247,7 @@ export function getTool(slug: string): Tool | undefined { } export const SITE_URL = "https://tools.codercops.com"; + +// The open-source repo. GITHUB_REPO is the "owner/name" slug used for the API. +export const GITHUB_REPO = "codercops/toolbelt"; +export const GITHUB_REPO_URL = `https://github.com/${GITHUB_REPO}`;