feat: add star button
This commit is contained in:
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<section className="space-y-6 py-12 sm:py-20 lg:py-24">
|
||||
@@ -38,23 +40,11 @@ export default async function HeroLanding() {
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center space-x-2">
|
||||
<Link
|
||||
href="/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
rounded: "xl",
|
||||
size: "lg",
|
||||
className: "bg-primary-foreground hover:opacity-70",
|
||||
}),
|
||||
"px-4 text-[15px]",
|
||||
)}
|
||||
>
|
||||
<Icons.bookOpen className="mr-2 size-4" />
|
||||
<p>Documents</p>
|
||||
</Link>
|
||||
<GitHubStarsWithSuspense
|
||||
owner="oiov"
|
||||
repo="wr.do"
|
||||
className="shadow-sm"
|
||||
/>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
prefetch={true}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// components/GitHubStarsButton.tsx
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { buttonVariants } from "../ui/button";
|
||||
|
||||
interface GitHubStarsButtonProps {
|
||||
owner: string;
|
||||
repo: string;
|
||||
className?: string;
|
||||
initialStars?: number;
|
||||
}
|
||||
|
||||
const GitHubStarsButton: React.FC<GitHubStarsButtonProps> = ({
|
||||
owner,
|
||||
repo,
|
||||
className = "",
|
||||
initialStars,
|
||||
}) => {
|
||||
const [stars, setStars] = useState<number>(initialStars ?? 0);
|
||||
const [loading, setLoading] = useState<boolean>(!initialStars);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<span className="text-sm text-red-500">Unable to load GitHub stars</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`https://github.com/${owner}/${repo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
rounded: "xl",
|
||||
size: "lg",
|
||||
className: "bg-primary-foreground hover:opacity-70",
|
||||
}),
|
||||
"px-4 text-[15px]",
|
||||
)}
|
||||
aria-label={`GitHub repository: ${owner}/${repo}`}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
className="mr-2 h-5 w-5"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
<span className="flex items-center gap-2 font-medium">
|
||||
GitHub
|
||||
{loading ? (
|
||||
<span className="h-7 w-20 animate-pulse rounded-[16px] bg-gray-200/80" />
|
||||
) : (
|
||||
<span className="animate-fade-in rounded-[16px] bg-gray-200/80 px-3 py-1 text-sm font-bold text-slate-600">
|
||||
{new Intl.NumberFormat().format(stars)} stars
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitHubStarsButton;
|
||||
@@ -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 (
|
||||
<GitHubStarsButton
|
||||
owner={owner}
|
||||
repo={repo}
|
||||
className={className}
|
||||
initialStars={stars}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 导出一个包装了 Suspense 的组件
|
||||
export default function GitHubStarsWithSuspense(props: Props) {
|
||||
return (
|
||||
<Suspense fallback={<GitHubStarsButton {...props} />}>
|
||||
<GitHubStarsWrapper {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user