mirror of
https://github.com/Dokploy/website.git
synced 2026-06-15 20:25:25 +02:00
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:
77
apps/website/app/api/github-stars/route.ts
Normal file
77
apps/website/app/api/github-stars/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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 Didn’t 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 we’re 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 people—who 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>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user