From dc442a2bf82845647f153c6480c2698c37242506 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 16:46:43 +0000 Subject: [PATCH] Add a site analytics strip to the sponsors page. GitHub stars are read from the repo once a day. Component count comes from the registry. Pageview, visitor, bounce, and time cards use the same layout and stay blank until those totals are available. Co-authored-by: Jay Sharma --- app/(marketing)/sponsors/page.tsx | 7 +- components/site-stats.tsx | 146 ++++++++++++++++++++++++++++++ lib/github.ts | 7 +- lib/site-stats.ts | 29 ++++++ tests/sponsors.test.mjs | 26 ++++++ 5 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 components/site-stats.tsx create mode 100644 lib/site-stats.ts diff --git a/app/(marketing)/sponsors/page.tsx b/app/(marketing)/sponsors/page.tsx index a593290..5ab692d 100644 --- a/app/(marketing)/sponsors/page.tsx +++ b/app/(marketing)/sponsors/page.tsx @@ -1,8 +1,10 @@ import { connection } from "next/server" import { Logo } from "@/components/logo" +import { SiteStats } from "@/components/site-stats" import { buttonVariants } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" +import { getGithubStars } from "@/lib/github" import { buildPageMetadata } from "@/lib/seo" import { getSponsorTiers, @@ -72,6 +74,7 @@ function SponsorSection({ tier }: { tier: SponsorTier }) { } export default async function SponsorsPage() { + const githubStars = await getGithubStars() await connection() const tiers = getSponsorTiers() @@ -91,7 +94,9 @@ export default async function SponsorsPage() {

-
+ + +
{tiers.map((tier) => ( ))} diff --git a/components/site-stats.tsx b/components/site-stats.tsx new file mode 100644 index 0000000..f9e2d73 --- /dev/null +++ b/components/site-stats.tsx @@ -0,0 +1,146 @@ +import Link from "next/link" + +import { getGithubRepoUrl } from "@/lib/github" +import { + COMPONENT_COUNT, + formatDuration, + formatExactCount, + formatPercent, + formatViewsPerVisit, +} from "@/lib/site-stats" +import { cn } from "@/lib/utils" + +const cardClassName = + "flex flex-col items-center justify-center rounded-2xl border border-border bg-card px-4 py-8 text-center" + +function SummaryCard({ + value, + label, + href, + external, +}: { + value: string + label: string + href?: string + external?: boolean +}) { + const body = ( + <> +

+ {value} +

+

{label}

+ + ) + + if (!href) { + return
{body}
+ } + + const className = cn( + cardClassName, + "min-h-36 transition-colors hover:bg-muted/40" + ) + + if (external) { + return ( + + {body} + + ) + } + + return ( + + {body} + + ) +} + +function MetricCard({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

+ {value} +

+

No previous range

+
+ ) +} + +export function SiteStats({ + githubStars, + pageviewsLastMonth = null, + pageviewsSinceLaunch = null, + visitors = null, + pageviews = null, + bounceRate = null, + avgTimeSeconds = null, + viewsPerVisit = null, +}: { + githubStars: number | null + pageviewsLastMonth?: number | null + pageviewsSinceLaunch?: number | null + visitors?: number | null + pageviews?: number | null + bounceRate?: number | null + avgTimeSeconds?: number | null + viewsPerVisit?: number | null +}) { + return ( +
+
+ + + + +
+ +
+

+ {formatExactCount(pageviewsSinceLaunch)} +

+

+ Pageviews since launch +

+
+ +
+ + + + + +
+ +

+ GitHub stars refresh daily. +

+
+ ) +} diff --git a/lib/github.ts b/lib/github.ts index d5c2789..46acedf 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -16,7 +16,10 @@ export function formatStarCount(count: number) { }).format(count) } -/** Cached star count for the docs header. Revalidates every 10 minutes. */ +/** Star count is read from GitHub once a day. */ +export const GITHUB_STARS_REVALIDATE_SECONDS = 60 * 60 * 24 + +/** Cached star count. Revalidates once a day. */ export async function getGithubStars(): Promise { try { const res = await fetch(`https://api.github.com/repos/${REPO}`, { @@ -24,7 +27,7 @@ export async function getGithubStars(): Promise { Accept: "application/vnd.github+json", "User-Agent": "23rd.dev", }, - next: { revalidate: 600 }, + next: { revalidate: GITHUB_STARS_REVALIDATE_SECONDS }, }) if (!res.ok) return null const data = (await res.json()) as { stargazers_count?: number } diff --git a/lib/site-stats.ts b/lib/site-stats.ts new file mode 100644 index 0000000..9702712 --- /dev/null +++ b/lib/site-stats.ts @@ -0,0 +1,29 @@ +import registry from "@/registry.json" + +export const COMPONENT_COUNT = registry.include.length + +export function formatExactCount(value: number | null): string { + if (value == null) return "—" + return new Intl.NumberFormat("en").format(value) +} + +export function formatPercent(value: number | null): string { + if (value == null) return "—" + return `${Math.round(value)}%` +} + +export function formatDuration(seconds: number | null): string { + if (seconds == null) return "—" + const rounded = Math.max(0, Math.round(seconds)) + if (rounded < 60) return `${rounded}s` + const minutes = Math.floor(rounded / 60) + const remainder = rounded % 60 + return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s` +} + +export function formatViewsPerVisit(value: number | null): string { + if (value == null) return "—" + return new Intl.NumberFormat("en", { + maximumFractionDigits: 1, + }).format(value) +} diff --git a/tests/sponsors.test.mjs b/tests/sponsors.test.mjs index 98a4d34..67e5245 100644 --- a/tests/sponsors.test.mjs +++ b/tests/sponsors.test.mjs @@ -6,6 +6,9 @@ import { join } from "node:path" import { ROOT } from "../scripts/registry-lib.mjs" const sponsorsLib = readFileSync(join(ROOT, "lib/sponsors.ts"), "utf8") +const githubLib = readFileSync(join(ROOT, "lib/github.ts"), "utf8") +const siteStats = readFileSync(join(ROOT, "components/site-stats.tsx"), "utf8") +const siteStatsLib = readFileSync(join(ROOT, "lib/site-stats.ts"), "utf8") const sponsorsPage = readFileSync( join(ROOT, "app/(marketing)/sponsors/page.tsx"), "utf8" @@ -196,3 +199,26 @@ test("sponsors prices, terms, privacy, and support email match the shadscan layo assert.match(nextConfig, /source: "\/pricing"/) assert.match(nextConfig, /destination: "\/sponsors"/) }) + +test("sponsors page lists analytics and reads GitHub stars once a day", () => { + assert.match(sponsorsPage, /