Skip to content

Commit b61ca64

Browse files
committed
feat(docs): add animated GitHub stars button to sidebar
Add a star count button to the docs sidebar footer that fetches the live GitHub star count via a cached API route and animates it with NumberFlow. Polls every 60 seconds, links to the repo on click. Also fix PostHog proxy 404s in local dev by mirroring the vercel.json rewrites in next.config.mjs.
1 parent 68b1449 commit b61ca64

File tree

6 files changed

+117
-0
lines changed

6 files changed

+117
-0
lines changed

docs/next.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ const config = {
2929
source: "/docs/:path*.mdx",
3030
destination: "/llms.mdx/docs/:path*",
3131
},
32+
{
33+
source: "/ph/static/:path*",
34+
destination: "https://us-assets.i.posthog.com/static/:path*",
35+
},
36+
{
37+
source: "/ph/:path*",
38+
destination: "https://us.i.posthog.com/:path*",
39+
},
3240
];
3341
},
3442
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { gitConfig } from "@/lib/layout.shared";
2+
3+
export async function GET() {
4+
const res = await fetch(`https://api.github.com/repos/${gitConfig.user}/${gitConfig.repo}`, {
5+
next: { revalidate: 300 },
6+
});
7+
8+
if (!res.ok) {
9+
return Response.json({ stars: null }, { status: 502 });
10+
}
11+
12+
const data = await res.json();
13+
const stars = data.stargazers_count;
14+
15+
return Response.json(
16+
{ stars },
17+
{
18+
headers: {
19+
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
20+
},
21+
},
22+
);
23+
}

docs/src/app/docs/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
AnimatedSidebarSeparator,
66
SidebarHoverProvider,
77
} from "@/components/docs-sidebar";
8+
import { GitHubStarsButton } from "@/components/github-stars-button";
89
import { baseOptions } from "@/lib/layout.shared";
910
import { source } from "@/lib/source";
1011

@@ -15,6 +16,7 @@ export default function Layout({ children }: LayoutProps<"/docs">) {
1516
tree={source.getPageTree()}
1617
{...baseOptions()}
1718
sidebar={{
19+
footer: <GitHubStarsButton />,
1820
components: {
1921
Item: AnimatedSidebarItem,
2022
Separator: AnimatedSidebarSeparator,

docs/src/app/global.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ figure.shiki {
8181
@apply md:border-0!;
8282
}
8383

84+
/* Sidebar footer: place icon links and star button on the same row */
85+
#nd-sidebar > div:last-child {
86+
flex-direction: row;
87+
align-items: center;
88+
flex-wrap: wrap;
89+
gap: 0.5rem;
90+
}
91+
8492
#nd-page {
8593
max-width: 1050px;
8694
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import { Star } from "lucide-react";
4+
import dynamic from "next/dynamic";
5+
import { useEffect, useState } from "react";
6+
import { gitConfig } from "@/lib/layout.shared";
7+
8+
const POLL_INTERVAL = 60_000;
9+
const GITHUB_URL = `https://github.com/${gitConfig.user}/${gitConfig.repo}`;
10+
11+
const AnimatedCount = dynamic(() => import("./github-stars-count"), {
12+
ssr: false,
13+
});
14+
15+
export function GitHubStarsButton() {
16+
const [stars, setStars] = useState<number | null>(null);
17+
18+
useEffect(() => {
19+
const controller = new AbortController();
20+
21+
async function fetchStars() {
22+
try {
23+
const res = await fetch("/api/github-stars", { signal: controller.signal });
24+
if (!res.ok) return;
25+
const data = await res.json();
26+
if (typeof data.stars === "number") setStars(data.stars);
27+
} catch {
28+
// Silently ignore aborts and network errors
29+
}
30+
}
31+
32+
fetchStars();
33+
const id = setInterval(fetchStars, POLL_INTERVAL);
34+
35+
return () => {
36+
controller.abort();
37+
clearInterval(id);
38+
};
39+
}, []);
40+
41+
const label = stars !== null ? `Star on GitHub (${stars} stars)` : "Star on GitHub";
42+
43+
return (
44+
<a
45+
href={GITHUB_URL}
46+
target="_blank"
47+
rel="noopener noreferrer"
48+
aria-label={label}
49+
className="group ms-auto inline-flex items-center gap-1.5 rounded-md border mt-1 border-white/[0.08] bg-white/[0.03] px-2 py-1.5 text-xs text-fd-muted-foreground transition-colors hover:border-[var(--brand-accent)]/30 hover:bg-[var(--brand-accent)]/[0.04] hover:text-fd-foreground"
50+
>
51+
<Star className="size-3.5 fill-[var(--brand-accent)] text-[var(--brand-accent)] transition-transform group-hover:scale-110" />
52+
53+
{stars !== null && (
54+
<span className="tabular-nums">
55+
<AnimatedCount stars={stars} />
56+
</span>
57+
)}
58+
</a>
59+
);
60+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"use client";
2+
3+
import "@/lib/rn-web-polyfills";
4+
5+
import { NumberFlow } from "number-flow-react-native";
6+
7+
export default function GitHubStarsCount({ stars }: { stars: number }) {
8+
return (
9+
<NumberFlow
10+
value={stars}
11+
style={{ fontSize: 12, color: "inherit" }}
12+
format={{ useGrouping: true }}
13+
trend={1}
14+
/>
15+
);
16+
}

0 commit comments

Comments
 (0)