feat: add API endpoint for fetching GitHub stars and update GithubStars component to utilize the new endpoint for dynamic star count

This commit is contained in:
Mauricio Siu
2025-11-18 11:59:24 -06:00
parent bde61bf0b4
commit 82e04c3b84
3 changed files with 177 additions and 12 deletions

View File

@@ -0,0 +1,77 @@
import { NextResponse } from "next/server";
// Cache the result for 5 minutes to avoid rate limiting
let cachedStars: { count: number; timestamp: number } | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const owner = searchParams.get("owner");
const repo = searchParams.get("repo");
if (!owner || !repo) {
return NextResponse.json(
{ error: "Owner and repo parameters are required" },
{ status: 400 },
);
}
// Check if we have a valid cached result
if (
cachedStars &&
Date.now() - cachedStars.timestamp < CACHE_DURATION
) {
return NextResponse.json(
{ stargazers_count: cachedStars.count },
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
},
);
}
try {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "Dokploy-Website",
},
},
);
if (!response.ok) {
return NextResponse.json(
{ error: "Failed to fetch repository data" },
{ status: response.status },
);
}
const data = await response.json();
const starCount = data.stargazers_count;
// Cache the result
cachedStars = {
count: starCount,
timestamp: Date.now(),
};
return NextResponse.json(
{ stargazers_count: starCount },
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
},
);
} catch (error) {
console.error("Error fetching GitHub stars:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}

View File

@@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
type GithubStarsProps = {
@@ -10,17 +11,73 @@ type GithubStarsProps = {
count?: string;
};
// Function to format star count (e.g., 26400 -> "26.4k")
function formatStarCount(count: number): string {
if (count >= 1000000) {
return `${(count / 1000000).toFixed(1)}M`;
}
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k`;
}
return count.toString();
}
// Extract owner and repo from GitHub URL
function extractRepoInfo(url: string): { owner: string; repo: string } | null {
try {
const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (match) {
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
}
} catch (error) {
console.error("Error extracting repo info:", error);
}
return null;
}
export function GithubStars({
className,
repoUrl = "https://github.com/dokploy/dokploy",
label = "GitHub Stars",
count = "26.4k",
count: defaultCount = "26.4k",
}: GithubStarsProps) {
const [starCount, setStarCount] = useState<string>(defaultCount);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchStarCount = async () => {
const repoInfo = extractRepoInfo(repoUrl);
if (!repoInfo) {
setIsLoading(false);
return;
}
try {
const response = await fetch(
`/api/github-stars?owner=${encodeURIComponent(repoInfo.owner)}&repo=${encodeURIComponent(repoInfo.repo)}`,
);
if (response.ok) {
const data = await response.json();
const formattedCount = formatStarCount(data.stargazers_count);
setStarCount(formattedCount);
}
} catch (error) {
console.error("Error fetching GitHub stars:", error);
// Keep default count on error
} finally {
setIsLoading(false);
}
};
fetchStarCount();
}, [repoUrl]);
return (
<Link
href={repoUrl}
target="_blank"
aria-label={`${label}: ${count}`}
aria-label={`${label}: ${starCount}`}
className={cn(
"group relative inline-flex items-center gap-2 rounded-full px-3 py-1",
"shadow-[0_0_0_2px_#000_inset,0_2px_8px_rgba(0,0,0,0.35)]",
@@ -109,7 +166,9 @@ export function GithubStars({
{/* copy */}
<span className="flex items-baseline gap-1 pr-0.5">
<span className="text-xs font-semibold">Stars</span>
<span className="text-sm font-extrabold tracking-tight">{count}</span>
<span className="text-sm font-extrabold tracking-tight">
{isLoading ? "..." : starCount}
</span>
</span>
{/* subtle ring on hover */}

View File

@@ -1,24 +1,48 @@
"use client";
import { HandCoins, Users } from "lucide-react";
import React from "react";
import React, { useEffect, useState } from "react";
import { useId } from "react";
import NumberTicker from "./ui/number-ticker";
const statsValues = {
githubStars: 26000,
dockerDownloads: 3500000,
dockerDownloads: 4000000,
contributors: 200,
sponsors: 50,
};
export function StatsSection() {
const [githubStars, setGithubStars] = useState(statsValues.githubStars);
useEffect(() => {
const fetchGitHubStars = async () => {
try {
const response = await fetch(
"/api/github-stars?owner=dokploy&repo=dokploy",
);
if (response.ok) {
const data = await response.json();
setGithubStars(data.stargazers_count);
}
} catch (error) {
console.error("Error fetching GitHub stars:", error);
// Keep default value on error
}
};
fetchGitHubStars();
}, []);
return (
<div className="py-20 lg:py-40 flex flex-col gap-10 px-4 ">
<div className="mx-auto max-w-2xl md:text-center">
<h2 className="font-display text-3xl tracking-tight sm:text-4xl text-center">
Stats You Didnt Ask For (But Secretly Love to See)
Stats You Didn't Ask For (But Secretly Love to See)
</h2>
<p className="mt-4 text-lg tracking-tight text-muted-foreground text-center">
Just a few numbers to show were not *completely* making this up.
Just a few numbers to show we're not *completely* making this up.
Turns out, Dokploy has actually helped a few peoplewho knew?
</p>
</div>
@@ -35,9 +59,13 @@ export function StatsSection() {
{feature.icon}
</p>
<p className="text-neutral-400 mt-4 text-base font-normal relative z-20">
{feature.description}
{typeof feature.description === "function"
? feature.description(githubStars)
: feature.description}
</p>
{feature.component}
{typeof feature.component === "function"
? feature.component(githubStars)
: feature.component}
</div>
))}
</div>
@@ -48,15 +76,16 @@ export function StatsSection() {
const grid = [
{
title: "GitHub Stars",
description: `With over ${(statsValues.githubStars / 1000).toFixed(1)}k stars on GitHub, Dokploy is trusted by developers worldwide. Explore our repositories and join our community!`,
description: (stars: number) =>
`With over ${(stars / 1000).toFixed(1)}k stars on GitHub, Dokploy is trusted by developers worldwide. Explore our repositories and join our community!`,
icon: (
<svg aria-hidden="true" className="h-6 w-6 fill-white">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
</svg>
),
component: (
component: (stars: number) => (
<p className="whitespace-pre-wrap text-2xl !font-semibold tracking-tighter mt-4">
<NumberTicker value={statsValues.githubStars} />+
<NumberTicker value={stars} />+
</p>
),
},