Skip to content

Commit 7542311

Browse files
committed
feat: add live github star link
1 parent 4e3bcc6 commit 7542311

4 files changed

Lines changed: 125 additions & 3 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import Link from "next/link";
2+
3+
import { PixelIcon } from "@/components/pixel-icon";
4+
import {
5+
formatStarCount,
6+
getGitHubRepoStats,
7+
githubRepoUrl,
8+
} from "@/lib/github";
9+
import { cn } from "@/lib/utils";
10+
11+
type GitHubStarLinkProps = {
12+
className?: string;
13+
};
14+
15+
const baseClassName =
16+
"inline-flex h-10 items-center gap-3 rounded-[4px] border border-white/15 bg-white/[0.04] px-3 text-sm font-medium leading-none text-white transition hover:border-white/30 hover:bg-white/[0.08] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#8232ff] focus-visible:ring-offset-2 focus-visible:ring-offset-black";
17+
18+
export async function GitHubStarLink({ className }: GitHubStarLinkProps) {
19+
const stats = await getGitHubRepoStats();
20+
const starLabel = stats ? formatStarCount(stats.stars) : null;
21+
const ariaLabel = stats
22+
? `Open Agent Device on GitHub, ${stats.stars.toLocaleString("en-US")} stars`
23+
: "Open Agent Device on GitHub";
24+
25+
return (
26+
<Link
27+
aria-label={ariaLabel}
28+
className={cn(baseClassName, className)}
29+
href={githubRepoUrl}
30+
rel="noreferrer"
31+
target="_blank"
32+
>
33+
<span>GitHub</span>
34+
<span className="flex items-center gap-1.5 text-white/65">
35+
<PixelIcon name="star" className="size-4 text-[#8232ff]" />
36+
<span>{starLabel ?? "Stars"}</span>
37+
</span>
38+
</Link>
39+
);
40+
}
41+
42+
export function GitHubStarLinkFallback({ className }: GitHubStarLinkProps) {
43+
return (
44+
<Link
45+
aria-label="Open Agent Device on GitHub"
46+
className={cn(baseClassName, className)}
47+
href={githubRepoUrl}
48+
rel="noreferrer"
49+
target="_blank"
50+
>
51+
<span>GitHub</span>
52+
<span className="flex items-center gap-1.5 text-white/65">
53+
<PixelIcon name="star" className="size-4 text-[#8232ff]" />
54+
<span>Stars</span>
55+
</span>
56+
</Link>
57+
);
58+
}

landing/src/components/pixel-icon.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Search } from "pixelarticons/react/Search";
2525
import { Ship } from "pixelarticons/react/Ship";
2626
import { Shuffle } from "pixelarticons/react/Shuffle";
2727
import { Smartphone } from "pixelarticons/react/Smartphone";
28+
import { Star } from "pixelarticons/react/Star";
2829
import { Terminal } from "pixelarticons/react/Terminal";
2930
import { Users } from "pixelarticons/react/Users";
3031
import { Video } from "pixelarticons/react/Video";
@@ -59,6 +60,7 @@ export type PixelIconName =
5960
| "ship"
6061
| "shuffle"
6162
| "snapshot"
63+
| "star"
6264
| "terminal"
6365
| "video";
6466

@@ -92,6 +94,7 @@ const iconComponents: Record<PixelIconName, IconComponent> = {
9294
ship: Ship,
9395
shuffle: Shuffle,
9496
snapshot: Image,
97+
star: Star,
9598
terminal: Terminal,
9699
video: Video,
97100
};

landing/src/components/site-header.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Link from "next/link";
2+
import { Suspense } from "react";
23

4+
import { GitHubStarLink, GitHubStarLinkFallback } from "@/components/github-star-link";
35
import { ButtonLink } from "@/components/ui/button";
46
import { navigationPages } from "@/lib/seo";
57

@@ -24,9 +26,14 @@ export function SiteHeader() {
2426
</Link>
2527
))}
2628
</nav>
27-
<ButtonLink href="#get-started" size="compact" className="hidden lg:inline-flex">
28-
Get Started
29-
</ButtonLink>
29+
<div className="hidden items-center gap-3 lg:flex">
30+
<Suspense fallback={<GitHubStarLinkFallback />}>
31+
<GitHubStarLink />
32+
</Suspense>
33+
<ButtonLink href="#get-started" size="compact">
34+
Get Started
35+
</ButtonLink>
36+
</div>
3037
<ButtonLink
3138
href="#get-started"
3239
size="compact"

landing/src/lib/github.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const repoApiUrl = "https://api.github.com/repos/callstackincubator/agent-device";
2+
3+
type GitHubRepoResponse = {
4+
stargazers_count?: number;
5+
};
6+
7+
export type GitHubRepoStats = {
8+
stars: number;
9+
};
10+
11+
export const githubRepoUrl = "https://github.com/callstackincubator/agent-device";
12+
13+
export async function getGitHubRepoStats(): Promise<GitHubRepoStats | null> {
14+
try {
15+
const token = process.env.GITHUB_TOKEN;
16+
const response = await fetch(repoApiUrl, {
17+
headers: {
18+
Accept: "application/vnd.github+json",
19+
...(token ? { Authorization: `Bearer ${token}` } : {}),
20+
},
21+
next: {
22+
revalidate: 60 * 60 * 4,
23+
},
24+
});
25+
26+
if (!response.ok) {
27+
return null;
28+
}
29+
30+
const repo = await response.json() as GitHubRepoResponse;
31+
32+
if (typeof repo.stargazers_count !== "number") {
33+
return null;
34+
}
35+
36+
return {
37+
stars: repo.stargazers_count,
38+
};
39+
} catch {
40+
return null;
41+
}
42+
}
43+
44+
export function formatStarCount(stars: number) {
45+
if (stars >= 1_000_000) {
46+
return `${(stars / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
47+
}
48+
49+
if (stars >= 1_000) {
50+
return `${(stars / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
51+
}
52+
53+
return new Intl.NumberFormat("en-US").format(stars);
54+
}

0 commit comments

Comments
 (0)