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,
},
});