feat: 添加 GitHub 统计数据展示功能

新增 GitHub 统计卡片和徽章组件,在项目卡片、详情页和侧边栏中展示 GitHub stars、forks 等统计数据

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-30 14:14:02 +08:00
co-authored by Claude
parent 443dc8370e
commit cae5686d2d
12 changed files with 1766 additions and 41 deletions
+57 -9
View File
@@ -220,24 +220,72 @@ python main.py</code></pre>
</a>
</div>
<div class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-2 gap-4">
<!-- 方案A: GitHub 专用统计卡片 - 分离 GitHub 数据和其他元数据 -->
<div class="mb-6">
<div class="bg-gray-100 dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 text-black dark:text-white p-4">
<h3 class="font-display text-xs uppercase tracking-widest mb-3 flex items-center gap-2">
<svg class="w-4 h-4 text-primary" fill="currentColor" viewBox="0 0 16 16">
<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-3.14-.95-3.14-.95-.43-.92-.1-1.25-.1-1.25.2-.05.41.08.95.08 2.76 1.89 3.78 1.89 3.78 1.69 2.88 4.44 2.05 5.53-.16.47-.86-.94-1.25-1.14-.42-.26-.89-.04-1.25.23-.89.65-2.18.95-3.3 1.01-.22.01-.44.05-.66.12-.26.11-.53.03-.72-.13-.22-.18-.44-.49-.44-.49-.25-.95-.08-1.25.23-.72.72-1.87 2.05-2.84 2.85-.13.11-.24.26-.24.42 0 .33.27.76.76.76 1.16 0 .87.72 1.96 2.05 2.4 2.82.2.06.43.09.65.05.31-.06.6-.17.87-.33.26-.16.48-.36.63-.57.23-.21.47-.44.64-.67.19-.26.35-.54.48-.83.11-.26.17-.54.17-.82 0-.47-.27-.91-.66-1.1-.67-.34-1.45-.51-2.32-.51-.88 0-1.67.18-2.35.53-.26.13-.5.25-.73.36-.22.1-.42.23-.58.37-.14.13-.26.26-.34.4-.07.14-.1.29-.1.44 0 .18.09.34.25.46.13.11.29.18.46.18.19 0 .38-.06.55-.17.15-.11.28-.24.39-.39.1-.15.18-.31.23-.48.05-.18.06-.36.06-.54 0-.3-.17-.57-.44-.74-.23-.17-.5-.26-.78-.26-.28 0-.55.09-.79.26-.23.17-.41.39-.54.64-.12.24-.17.5-.17.77 0 .26.17.5.43.67.25.17.57.26.89.26.31 0 .6-.09.85-.26.23-.17.41-.39.54-.64.12-.24.18-.5.18-.77 0-.26-.17-.5-.43-.67-.26-.17-.57-.26-.89-.26-.31 0-.6.09-.85.26-.23.17-.41.39-.54.64-.12.24-.17.5-.17.77zM8 15c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z"/>
</svg>
GitHub Statistics
</h3>
<!-- 使用 Shields.io 徽章网格显示所有 GitHub 实时数据 -->
<div class="grid grid-cols-2 gap-3">
<a href="https://github.com/agentpark/autogpt-x/stargazers" class="group" title="View stars on GitHub">
<div class="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg class="w-4 h-4 text-primary" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
<div>
<span class="text-xs text-gray-500 uppercase font-mono block mb-1">License</span>
<span class="font-bold text-sm">MIT</span>
<div class="text-[9px] uppercase font-mono text-gray-500">Stars</div>
<div class="font-display font-bold text-lg">142k</div>
</div>
</div>
</a>
<a href="https://github.com/agentpark/autogpt-x/network/members" class="group" title="View forks on GitHub">
<div class="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 16 16">
<path d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
<div>
<span class="text-xs text-gray-500 uppercase font-mono block mb-1">Pricing</span>
<span class="font-bold text-sm">Free</span>
<div class="text-[9px] uppercase font-mono text-gray-500">Forks</div>
<div class="font-display font-bold text-lg">34k</div>
</div>
</div>
</a>
<a href="https://github.com/agentpark/autogpt-x/issues" class="group" title="View issues on GitHub">
<div class="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg class="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
<path d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"/>
</svg>
<div>
<span class="text-xs text-gray-500 uppercase font-mono block mb-1">Stars</span>
<span class="font-bold text-sm">142k</span>
<div class="text-[9px] uppercase font-mono text-gray-500">Issues</div>
<div class="font-display font-bold text-lg">892</div>
</div>
</div>
</a>
<a href="https://github.com/agentpark/autogpt-x/blob/main/LICENSE" class="group" title="View license on GitHub">
<div class="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg class="w-4 h-4 text-purple-600" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1.75c4.556 0 8.25 3.694 8.25 8.25 0 1.356-.344 2.647-.956 3.78-.556.057-1.072.25-1.444.525-.436.32-.688.742-.688 1.25 0 .469.253.891.688 1.25.47.4.869.496 1.444.525 2.09-.613 3.325-1.438 4.188-.47.48-1.088.79-1.5.79-.406-.006-.738-.274-.96-.638-.31-.494-.672-.59-1.031-.59-.25 0-.476.088-.656.25-.182.163-.39.25-.625.25-.238 0-.447-.087-.625-.25-.178-.164-.395-.25-.641-.25-.356 0-.722.096-1.031.59-.413.584-.554.29-1.5-.79-.612.313-1.156.48-1.625.525-.47-.22-.906-.25-1.444-.525-.612-1.133-.956-3.78-.956-4.556 0-8.25 3.694-8.25 8.25zM4.5 9a.5.5 0 01.5-.5h6a.5.5 0 010 1h-6a.5.5 0 01-.5-.5z"/>
</svg>
<div>
<span class="text-xs text-gray-500 uppercase font-mono block mb-1">Forks</span>
<span class="font-bold text-sm">34k</span>
<div class="text-[9px] uppercase font-mono text-gray-500">License</div>
<div class="font-display font-bold text-lg">MIT</div>
</div>
</div>
</a>
</div>
<div class="mt-3 pt-3 border-t border-gray-300 dark:border-gray-600">
<div class="flex items-center justify-between text-xs font-mono">
<span class="text-gray-500">Last commit</span>
<span class="font-display font-bold">2 days ago</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bg-primary border-2 border-black p-6 relative overflow-hidden shadow-brutal dark:shadow-none group cursor-pointer hover:bg-yellow-400 transition-colors">
+9
View File
@@ -172,9 +172,18 @@
<span class="bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 px-2 py-1 text-[10px] uppercase font-display font-bold text-gray-600 dark:text-gray-300">Automation</span>
<span class="bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 px-2 py-1 text-[10px] uppercase font-display font-bold text-gray-600 dark:text-gray-300">Workflow</span>
</div>
<div class="flex items-center justify-between">
<a class="inline-flex items-center font-display font-bold text-sm uppercase border-b-2 border-transparent hover:border-primary transition-colors pb-0.5" href="#">
View Details <span class="material-icons-outlined ml-1 text-sm">arrow_forward</span>
</a>
<!-- Neo-brutalism Stars Badge Design -->
<div class="inline-flex items-center gap-2 px-3 py-1.5 border-2 border-black dark:border-white/40 bg-white dark:bg-surface-dark shadow-neo-sm hover:shadow-neo transition-all cursor-pointer group/stars" title="42.5k stars on GitHub">
<svg class="w-4 h-4 text-primary" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
<span class="font-display font-bold text-sm text-black dark:text-white">42.5k</span>
</div>
</div>
</div>
</div>
<div class="bg-white dark:bg-surface-dark border-2 border-black dark:border-white/20 p-6 shadow-neo hover:shadow-neo-hover hover:-translate-y-1 transition-all duration-300 flex flex-col justify-between group">
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -7,7 +7,8 @@ const nextConfig = {
images: {
remotePatterns: [
{ hostname: 'localhost' },
{ hostname: '*.anthropic.com' }
{ hostname: '*.anthropic.com' },
{ hostname: 'img.shields.io' }
]
},
experimental: {
+1 -1
View File
@@ -33,7 +33,7 @@ export default async function ProjectDetailPage({
return (
<main className="min-h-screen bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark font-body transition-colors duration-200">
<div className="container mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-12">
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-12">
{/* Back Button */}
<div className="mb-8">
<a
+74
View File
@@ -0,0 +1,74 @@
import Image from 'next/image'
interface GitHubBadgesProps {
starsUrl?: string | null
size?: 'sm' | 'md' | 'lg'
className?: string
}
const sizes = {
sm: { width: 80, height: 20 },
md: { width: 100, height: 20 },
lg: { width: 120, height: 20 }
}
/**
* GitHub Stars 徽章组件 - 显示 GitHub Stars 数量
* 适用于项目详情页
*/
export function GitHubBadges({
starsUrl,
size = 'md',
className = ''
}: GitHubBadgesProps) {
if (!starsUrl) {
return null
}
const { width, height } = sizes[size]
return (
<div className={className}>
<Image
src={starsUrl}
alt="GitHub Stars"
width={width}
height={height}
unoptimized
className="hover:opacity-80 transition-opacity rounded"
/>
</div>
)
}
/**
* GitHub Stars 紧凑型组件 - 用于项目卡片
* 在较小的空间内显示 GitHub Stars 数量
*/
export function GitHubStatsCompact({
starsUrl,
className = ''
}: {
starsUrl?: string | null
className?: string
}) {
if (!starsUrl) {
return null
}
return (
<div className={`flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 ${className}`}>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
<Image
src={starsUrl}
alt="Stars"
width={60}
height={20}
unoptimized
className="rounded"
/>
</div>
)
}
@@ -0,0 +1,138 @@
import Link from 'next/link'
import Image from 'next/image'
/**
* GitHub Statistics 数据接口 - 支持文本值或徽章 URL
*/
export interface GitHubTextStats {
owner?: string | null
repo?: string | null
stars?: string | null // 文本值(如 "142k")或徽章 URL
forks?: string | null // 文本值(如 "34k")或徽章 URL
issues?: string | null // 文本值(如 "892")或徽章 URL
license?: string | null // 文本值(如 "MIT")或徽章 URL
}
interface GitHubTextStatsCardProps {
stats: GitHubTextStats
className?: string
}
/**
* 检查字符串是否是 URL
*/
function isUrl(str: string): boolean {
return str.startsWith('http://') || str.startsWith('https://')
}
/**
* 渲染统计值 - 支持文本或徽章图片
*/
function renderStatValue(value: string) {
if (isUrl(value)) {
return (
<Image
src={value}
alt="Stat"
width={70}
height={20}
unoptimized
className="h-5 w-auto"
/>
)
}
return <div className="font-display font-bold text-lg leading-tight">{value}</div>
}
/**
* GitHub Statistics Card Component - 混合模式版本
* 支持文本显示和徽章图片显示,符合 Neo-brutalism 设计风格
*
* 设计原型来源: design/detail.html 第 223-289 行
*/
export function GitHubTextStatsCard({ stats, className = '' }: GitHubTextStatsCardProps) {
// 如果没有任何统计数据,不显示卡片
if (!stats.stars && !stats.forks && !stats.issues && !stats.license) {
return null
}
return (
<div className={`bg-gray-100 dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 text-black dark:text-white p-4 ${className}`}>
{/* Header */}
<h3 className="font-display text-xs uppercase tracking-widest mb-3 flex items-center gap-2">
<svg className="w-4 h-4 text-primary" fill="currentColor" viewBox="0 0 16 16">
<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-3.14-.95-3.14-.95-.43-.92-.1-1.25-.1-1.25.2-.05.41.08.95.08 2.76 1.89 3.78 1.89 3.78 1.69 2.88 4.44 2.05 5.53-.16.47-.86-.94-1.25-1.14-.42-.26-.89-.04-1.25.23-.89.65-2.18.95-3.3 1.01-.22.01-.44.05-.66.12-.26.11-.53.03-.72-.13-.22-.18-.44-.49-.44-.49-.25-.95-.08-1.25.23-.72.72-1.87 2.05-2.84 2.85-.13.11-.24.26-.24.42 0 .33.27.76.76.76 1.16 0 .87.72 1.96 2.05 2.4 2.82.2.06.43.09.65.05.31-.06.6-.17.87-.33.26-.16.48-.36.63-.57.23-.21.47-.44.64-.67.19-.26.35-.54.48-.83.11-.26.17-.54.17-.82 0-.47-.27-.91-.66-1.1-.67-.34-1.45-.51-2.32-.51-.88 0-1.67.18-2.35.53-.26.13-.5.25-.73.36-.22.1-.42.23-.58.37-.14.13-.26.26-.34.4-.07.14-.1.29-.1.44 0 .18.09.34.25.46.13.11.29.18.46.18.19 0 .38-.06.55-.17.15-.11.28-.24.39-.39.1-.15.18-.31.23-.48.05-.18.06-.36.06-.54 0-.3-.17-.57-.44-.74-.23-.17-.5-.26-.78-.26-.28 0-.55.09-.79.26-.23.17-.41.39-.54.64-.12.24-.17.5-.17.77 0 .26.17.5.43.67.25.17.57.26.89.26.31 0 .6-.09.85-.26.23-.17.41-.39.54-.64.12-.24.18-.5.18-.77 0-.26-.17-.5-.43-.67-.26-.17-.57-.26-.89-.26-.31 0-.6.09-.85.26-.23.17-.41.39-.54-.64-.12.24-.17.5-.17.77zM8 15c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7z"/>
</svg>
GitHub Statistics
</h3>
{/* Stats Grid - 2 columns */}
<div className="grid grid-cols-2 gap-3">
{/* Stars */}
{stats.stars && stats.owner && stats.repo && (
<Link
href={`https://github.com/${stats.owner}/${stats.repo}/stargazers`}
className="group"
title="View stars on GitHub"
>
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg className="w-4 h-4 text-primary flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
{renderStatValue(stats.stars)}
</div>
</Link>
)}
{/* Forks */}
{stats.forks && stats.owner && stats.repo && (
<Link
href={`https://github.com/${stats.owner}/${stats.repo}/network/members`}
className="group"
title="View forks on GitHub"
>
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg className="w-4 h-4 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
<path d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0V.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
{renderStatValue(stats.forks)}
</div>
</Link>
)}
{/* Issues */}
{stats.issues && stats.owner && stats.repo && (
<Link
href={`https://github.com/${stats.owner}/${stats.repo}/issues`}
className="group"
title="View issues on GitHub"
>
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg className="w-4 h-4 text-green-600 flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
<path d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"/>
</svg>
{renderStatValue(stats.issues)}
</div>
</Link>
)}
{/* License */}
{stats.license && stats.owner && stats.repo && (
<Link
href={`https://github.com/${stats.owner}/${stats.repo}/blob/main/LICENSE`}
className="group"
title="View license on GitHub"
>
<div className="flex items-center gap-2 hover:bg-white dark:hover:bg-black hover:bg-opacity-50 p-2 rounded transition-colors">
<svg className="w-4 h-4 text-purple-600 flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1.75c4.556 0 8.25 3.694 8.25 8.25 0 1.356-.344 2.647-.956 3.78-.556.057-1.072.25-1.444.525-.436.32-.688.742-.688 1.25 0 .469.253.891.688 1.25.47.4.869.496 1.444.525 2.09-.613 3.325-1.438 4.188-.47.48-1.088.79-1.5.79-.406-.006-.738-.274-.96-.638-.31-.494-.672-.59-1.031-.59-.25 0-.476.088-.656.25-.182.163-.39.25-.625.25-.238 0-.447-.087-.625-.25-.178-.164-.395-.25-.641-.25-.356 0-.722.096-1.031.59-.413.584-.554.29-1.5-.79-.612.313-1.156.48-1.625.525-.47-.22-.906-.25-1.444-.525-.612-1.133-.956-3.78-.956-4.556 0-8.25 3.694-8.25 8.25zM4.5 9a.5.5 0 01.5-.5h6a.5.5 0 010 1h-6a.5.5 0 01-.5-.5z"/>
</svg>
{renderStatValue(stats.license)}
</div>
</Link>
)}
</div>
</div>
)
}
+40 -8
View File
@@ -1,5 +1,8 @@
import Link from 'next/link'
import Image from 'next/image'
import { getTranslations } from 'next-intl/server'
import { GitHubStatsCompact } from './GitHubBadges'
import { getGitHubBadgesFromLinks } from '@/lib/github/badges'
interface ProjectCardProps {
project: {
@@ -15,6 +18,12 @@ interface ProjectCardProps {
nameEn?: string | null
slug: string
}>
links?: Array<{
id: string
type: string
url: string
title?: string | null
}>
}
locale: string
featured?: boolean
@@ -37,6 +46,9 @@ export async function ProjectCard({ project, locale, featured = false }: Project
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
const displayDescription = locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
// Generate GitHub badge URLs
const badges = project.links ? getGitHubBadgesFromLinks(project.links as any) : { stars: null, forks: null }
// Helper to get display name for tag based on locale
const getTagName = (tag: { name: string; nameEn?: string | null }) => {
return locale === 'en' && tag.nameEn ? tag.nameEn : tag.name
@@ -73,14 +85,34 @@ export async function ProjectCard({ project, locale, featured = false }: Project
))}
</div>
{/* View Details Link */}
<Link
href={`/${locale}/projects/${project.slug}`}
className="inline-flex items-center font-display font-bold text-sm uppercase hover:text-secondary dark:hover:text-primary group"
>
{t('viewDetails')}{' '}
<span className="ml-1 transform group-hover:translate-x-1 transition-transform"></span>
</Link>
{/* Footer with View Details and Stars */}
<div className="flex items-center justify-between mt-auto">
{/* View Details Link */}
<Link
href={`/${locale}/projects/${project.slug}`}
className="inline-flex items-center font-display font-bold text-sm uppercase hover:text-secondary dark:hover:text-primary group"
>
{t('viewDetails')}{' '}
<span className="ml-1 transform group-hover:translate-x-1 transition-transform"></span>
</Link>
{/* GitHub Stars Badge */}
{badges.stars && (
<div className="flex items-center gap-1 text-sm text-gray-600 dark:text-gray-400">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
<Image
src={badges.stars}
alt="Stars"
width={70}
height={20}
unoptimized
className="rounded"
/>
</div>
)}
</div>
</div>
</article>
)
+5
View File
@@ -2,6 +2,8 @@ import Link from 'next/link'
import { getTranslations } from 'next-intl/server'
import { MarkdownContent } from './MarkdownContent'
import { ShareButtons } from './ShareButtons'
import { GitHubBadges } from './GitHubBadges'
import { getGitHubBadgesFromLinks } from '@/lib/github/badges'
interface ProjectDetailProps {
project: {
@@ -54,6 +56,9 @@ export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
const displayContent = locale === 'en' && project.contentEn ? project.contentEn : project.content
// Generate GitHub badge URLs
const badges = getGitHubBadgesFromLinks(project.links as any)
// Get category from first tag
const category = project.tags[0]?.name || 'AI Agent'
const categoryEn = project.tags[0]?.nameEn || project.tags[0]?.name || 'AI Agent'
+21 -22
View File
@@ -1,5 +1,7 @@
import Link from 'next/link'
import { getTranslations } from 'next-intl/server'
import { GitHubTextStatsCard } from './GitHubTextStatsCard'
import { getGitHubInfoFromLinks } from '@/lib/github/badges'
interface ProjectSidebarProps {
project: {
@@ -37,6 +39,9 @@ export async function ProjectSidebar({ project, locale }: ProjectSidebarProps) {
const t = await getTranslations('project')
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
// 获取 GitHub 统计数据
const githubInfo = getGitHubInfoFromLinks(project.links)
return (
<aside className="space-y-8">
{/* Project Links Card */}
@@ -74,31 +79,25 @@ export async function ProjectSidebar({ project, locale }: ProjectSidebarProps) {
)
})}
</div>
{/* Stats Section */}
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-xs text-gray-500 uppercase font-mono block mb-1">{t('license')}</span>
<span className="font-bold text-sm">MIT</span>
</div>
<div>
<span className="text-xs text-gray-500 uppercase font-mono block mb-1">{t('pricing')}</span>
<span className="font-bold text-sm">Free</span>
</div>
<div>
<span className="text-xs text-gray-500 uppercase font-mono block mb-1">{t('status')}</span>
<span className="font-bold text-sm">Active</span>
</div>
<div>
<span className="text-xs text-gray-500 uppercase font-mono block mb-1">{t('type')}</span>
<span className="font-bold text-sm">{t('openSource')}</span>
</div>
</div>
</div>
</div>
)}
{/* GitHub Statistics Card */}
{githubInfo.owner && githubInfo.repo && (
<GitHubTextStatsCard
stats={{
owner: githubInfo.owner,
repo: githubInfo.repo,
// 注意:目前使用徽章 URL,后续可以通过 API 获取实际文本数据
// 如需显示实际数字,需要使用 GitHub API 或服务端获取
stars: githubInfo.stars,
forks: githubInfo.forks,
issues: githubInfo.issues,
license: githubInfo.license
}}
/>
)}
{/* CTA Card */}
<div className="bg-primary border-2 border-black p-6 relative overflow-hidden shadow-brutal dark:shadow-none group cursor-pointer hover:bg-yellow-400 transition-colors">
<div className="absolute top-0 right-0 w-20 h-20 bg-white opacity-20 transform rotate-45 translate-x-10 -translate-y-10"></div>
+92
View File
@@ -0,0 +1,92 @@
/**
* GitHub API 服务
* 获取仓库的统计数据(stars, forks, issues, license 等)
*/
export interface GitHubStats {
stargazers_count: number
forks_count: number
open_issues_count: number
license: { key: string; name: string } | null
pushed_at: string
}
/**
* 从 GitHub API 获取仓库统计信息
* @param owner - 仓库所有者
* @param repo - 仓库名称
* @returns GitHub 统计数据或 null
*/
export async function getGitHubStats(
owner: string,
repo: string
): Promise<GitHubStats | null> {
try {
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
headers: {
Accept: 'application/vnd.github.v3+json',
// 如果需要更高的速率限制,可以添加 GitHub token
// Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
next: { revalidate: 300 } // 缓存 5 分钟
})
if (!response.ok) {
console.error(`GitHub API error: ${response.status}`)
return null
}
const data = await response.json()
return {
stargazers_count: data.stargazers_count || 0,
forks_count: data.forks_count || 0,
open_issues_count: data.open_issues_count || 0,
license: data.license || null,
pushed_at: data.pushed_at || ''
}
} catch (error) {
console.error('Failed to fetch GitHub stats:', error)
return null
}
}
/**
* 格式化数字(如 142000 -> 142k
*/
export function formatNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'
}
if (num >= 1000) {
return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'k'
}
return num.toString()
}
/**
* 格式化相对时间(如 "2 days ago"
*/
export function formatRelativeTime(dateString: string, locale: string = 'zh'): string {
if (!dateString) return ''
const date = new Date(dateString)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (locale === 'en') {
if (diffDays === 0) return 'today'
if (diffDays === 1) return 'yesterday'
if (diffDays < 7) return `${diffDays} days ago`
if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`
return `${Math.floor(diffDays / 365)} years ago`
} else {
if (diffDays === 0) return '今天'
if (diffDays === 1) return '昨天'
if (diffDays < 7) return `${diffDays} 天前`
if (diffDays < 30) return `${Math.floor(diffDays / 7)} 周前`
if (diffDays < 365) return `${Math.floor(diffDays / 30)} 月前`
return `${Math.floor(diffDays / 365)} 年前`
}
}
+140
View File
@@ -0,0 +1,140 @@
import { ExternalLink, LinkType } from '@prisma/client'
/**
* 从 GitHub URL 提取 owner 和 repo
* @param url - GitHub 仓库 URL
* @returns owner 和 repo,或 null 如果无法解析
*/
export function parseGitHubUrl(url: string): { owner: string; repo: string } | null {
const patterns = [
/github\.com\/([^\/]+)\/([^\/\?#]+)/,
/github\.com\/([^\/]+)\/([^\/\?#]+)\.git/
]
for (const pattern of patterns) {
const match = url.match(pattern)
if (match) {
const repo = match[2].replace(/\.git$/, '').replace(/\/$/, '')
return { owner: match[1], repo }
}
}
return null
}
/**
* 生成 GitHub Stars 徽章 URL
* @param owner - 仓库所有者
* @param repo - 仓库名称
* @returns Shields.io 徽章 URL
*/
export function getGitHubStarsBadgeUrl(owner: string, repo: string): string {
return `https://img.shields.io/github/stars/${owner}/${repo}?style=social`
}
/**
* 生成所有 GitHub 徽章 URL
* @param owner - 仓库所有者
* @param repo - 仓库名称
* @returns 包含所有徽章 URL 的对象
*/
export function getAllGitHubBadgeUrls(owner: string, repo: string): {
stars: string
forks: string
issues: string
license: string
lastCommit: string
} {
return {
stars: `https://img.shields.io/github/stars/${owner}/${repo}?style=social`,
forks: `https://img.shields.io/github/forks/${owner}/${repo}?style=social`,
issues: `https://img.shields.io/github/issues/${owner}/${repo}`,
license: `https://img.shields.io/github/license/${owner}/${repo}`,
lastCommit: `https://img.shields.io/github/last-commit/${owner}/${repo}`
}
}
/**
* 从项目链接中提取 GitHub Stars 徽章 URL
* @param links - 项目外部链接数组
* @returns stars 徽章 URL 或 null
*/
export function getGitHubBadgesFromLinks(
links: ExternalLink[]
): { stars: string | null } {
const githubLink = links.find(link => link.type === 'GITHUB')
if (!githubLink) {
return { stars: null }
}
const parsed = parseGitHubUrl(githubLink.url)
if (!parsed) {
return { stars: null }
}
return {
stars: getGitHubStarsBadgeUrl(parsed.owner, parsed.repo)
}
}
/**
* 从项目链接中提取 GitHub 信息(用于文本显示)
* @param links - 项目外部链接数组
* @returns GitHub 统计数据对象
*/
export function getGitHubInfoFromLinks(
links: Array<{ type: string; url: string }>
): {
owner: string | null
repo: string | null
stars: string | null
forks: string | null
issues: string | null
license: string | null
lastCommit: string | null
} {
const githubLink = links.find(link => link.type === 'GITHUB')
if (!githubLink) {
return {
owner: null,
repo: null,
stars: null,
forks: null,
issues: null,
license: null,
lastCommit: null
}
}
const parsed = parseGitHubUrl(githubLink.url)
if (!parsed) {
return {
owner: null,
repo: null,
stars: null,
forks: null,
issues: null,
license: null,
lastCommit: null
}
}
// 返回徽章 URL,组件中可以使用这些 URL 获取实际数据
// 或者通过 GitHub API 获取实际数字
const badgeUrls = getAllGitHubBadgeUrls(parsed.owner, parsed.repo)
return {
owner: parsed.owner,
repo: parsed.repo,
// 暂时返回徽章 URL,后续可以通过 API 获取实际数字
stars: badgeUrls.stars,
forks: badgeUrls.forks,
issues: badgeUrls.issues,
license: badgeUrls.license,
lastCommit: badgeUrls.lastCommit
}
}