From b69db631692e44ee6d6ac9d6a52d87cb7e2c7d1c Mon Sep 17 00:00:00 2001 From: oiov Date: Sat, 2 Nov 2024 15:48:14 +0800 Subject: [PATCH] feat: add star button --- app/api/github/route.ts | 77 ++++++++++++++++ components/sections/hero-landing.tsx | 24 ++--- components/shared/github-star-button.tsx | 107 ++++++++++++++++++++++ components/shared/github-star-wrapper.tsx | 60 ++++++++++++ env.mjs | 2 + 5 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 app/api/github/route.ts create mode 100644 components/shared/github-star-button.tsx create mode 100644 components/shared/github-star-wrapper.tsx diff --git a/app/api/github/route.ts b/app/api/github/route.ts new file mode 100644 index 0000000..76d00c3 --- /dev/null +++ b/app/api/github/route.ts @@ -0,0 +1,77 @@ +// app/api/github-stars/route.ts +import { NextRequest, NextResponse } from "next/server"; + +import { env } from "@/env.mjs"; + +interface GitHubResponse { + stargazers_count: number; + message?: string; +} + +export async function GET(request: NextRequest) { + // 从 URL 中获取查询参数 + const searchParams = request.nextUrl.searchParams; + const owner = searchParams.get("owner"); + const repo = searchParams.get("repo"); + + // 验证必需的参数 + if (!owner || !repo) { + return NextResponse.json( + { error: "Invalid owner or repo parameters" }, + { status: 400 }, + ); + } + + try { + const { GITHUB_TOKEN } = env; + + if (!GITHUB_TOKEN) { + throw new Error("GitHub token is not configured"); + } + + const response = await fetch( + `https://api.github.com/repos/${owner}/${repo}`, + { + headers: { + Accept: "application/vnd.github.v3+json", + Authorization: `token ${GITHUB_TOKEN}`, + "User-Agent": "NextJS-App", + }, + // 添加缓存策略 + next: { + revalidate: 3600, // 1小时后重新验证 + }, + }, + ); + + if (!response.ok) { + const errorData: GitHubResponse = await response.json(); + throw new Error( + errorData.message || `HTTP error! status: ${response.status}`, + ); + } + + const data: GitHubResponse = await response.json(); + + return NextResponse.json( + { stars: data.stargazers_count }, + { + headers: { + "Cache-Control": + "public, s-maxage=3600, stale-while-revalidate=86400", + }, + }, + ); + } catch (error) { + console.error("GitHub API error:", error); + return NextResponse.json( + { + error: + error instanceof Error + ? error.message + : "Failed to fetch GitHub stars", + }, + { status: 500 }, + ); + } +} diff --git a/components/sections/hero-landing.tsx b/components/sections/hero-landing.tsx index 9559ebf..8000f77 100644 --- a/components/sections/hero-landing.tsx +++ b/components/sections/hero-landing.tsx @@ -7,6 +7,8 @@ import { cn, nFormatter } from "@/lib/utils"; import { buttonVariants } from "@/components/ui/button"; import { Icons } from "@/components/shared/icons"; +import GitHubStarsWithSuspense from "../shared/github-star-wrapper"; + export default async function HeroLanding() { return (
@@ -38,23 +40,11 @@ export default async function HeroLanding() {

- - -

Documents

- + = ({ + owner, + repo, + className = "", + initialStars, +}) => { + const [stars, setStars] = useState(initialStars ?? 0); + const [loading, setLoading] = useState(!initialStars); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchStars = async () => { + // 如果有初始值,不需要立即获取 + if (initialStars !== undefined) return; + + try { + const response = await fetch( + `/api/github?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`, + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.error || `HTTP error! status: ${response.status}`, + ); + } + + const data = await response.json(); + setStars(data.stars); + setError(null); + } catch (error) { + console.error("Error fetching GitHub stars:", error); + setError( + error instanceof Error ? error.message : "Failed to fetch stars", + ); + } finally { + setLoading(false); + } + }; + + if (owner && repo) { + fetchStars(); + } + }, [owner, repo, initialStars]); + + if (error) { + return ( + Unable to load GitHub stars + ); + } + + return ( + + + + GitHub + {loading ? ( + + ) : ( + + {new Intl.NumberFormat().format(stars)} stars + + )} + + + ); +}; + +export default GitHubStarsButton; diff --git a/components/shared/github-star-wrapper.tsx b/components/shared/github-star-wrapper.tsx new file mode 100644 index 0000000..b474428 --- /dev/null +++ b/components/shared/github-star-wrapper.tsx @@ -0,0 +1,60 @@ +// components/GitHubStarsWrapper.tsx +import { Suspense } from "react"; + +import GitHubStarsButton from "./github-star-button"; + +interface GitHubResponse { + stargazers_count: number; +} + +async function getGitHubStars(owner: string, repo: string) { + const githubToken = process.env.GITHUB_TOKEN; + + if (!githubToken) { + throw new Error("GitHub token is not configured"); + } + + const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { + headers: { + Accept: "application/vnd.github.v3+json", + Authorization: `token ${githubToken}`, + "User-Agent": "NextJS-App", + }, + next: { revalidate: 3600 }, + }); + + if (!res.ok) { + throw new Error("Failed to fetch GitHub stars"); + } + + const data: GitHubResponse = await res.json(); + return data.stargazers_count; +} + +interface Props { + owner: string; + repo: string; + className?: string; +} + +async function GitHubStarsWrapper({ owner, repo, className }: Props) { + const stars = await getGitHubStars(owner, repo); + + return ( + + ); +} + +// 导出一个包装了 Suspense 的组件 +export default function GitHubStarsWithSuspense(props: Props) { + return ( + }> + + + ); +} diff --git a/env.mjs b/env.mjs index 7ea8375..2c8228e 100644 --- a/env.mjs +++ b/env.mjs @@ -17,6 +17,7 @@ export const env = createEnv({ CLOUDFLARE_API_KEY: z.string().min(1), CLOUDFLARE_EMAIL: z.string().min(1), SCREENSHOTONE_BASE_URL: z.string().min(1), + GITHUB_TOKEN: z.string().min(1), }, client: { NEXT_PUBLIC_APP_URL: z.string().min(1), @@ -41,5 +42,6 @@ export const env = createEnv({ CLOUDFLARE_API_KEY: process.env.CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL: process.env.CLOUDFLARE_EMAIL, SCREENSHOTONE_BASE_URL: process.env.SCREENSHOTONE_BASE_URL, + GITHUB_TOKEN: process.env.GITHUB_TOKEN, }, });