File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 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+ }
Original file line number Diff line number Diff line change @@ -25,6 +25,7 @@ import { Search } from "pixelarticons/react/Search";
2525import { Ship } from "pixelarticons/react/Ship" ;
2626import { Shuffle } from "pixelarticons/react/Shuffle" ;
2727import { Smartphone } from "pixelarticons/react/Smartphone" ;
28+ import { Star } from "pixelarticons/react/Star" ;
2829import { Terminal } from "pixelarticons/react/Terminal" ;
2930import { Users } from "pixelarticons/react/Users" ;
3031import { 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} ;
Original file line number Diff line number Diff line change 11import Link from "next/link" ;
2+ import { Suspense } from "react" ;
23
4+ import { GitHubStarLink , GitHubStarLinkFallback } from "@/components/github-star-link" ;
35import { ButtonLink } from "@/components/ui/button" ;
46import { 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"
Original file line number Diff line number Diff line change 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+ }
You can’t perform that action at this time.
0 commit comments