feat(web): add leaderboard UI page
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
This commit is contained in:
148
apps/web/src/app/leaderboard/page.tsx
Normal file
148
apps/web/src/app/leaderboard/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import Link from "next/link";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function LeaderboardPage() {
|
||||
const stats = await prisma.task.groupBy({
|
||||
by: ['builder_id'],
|
||||
where: {
|
||||
status: 'COMPLETED',
|
||||
builder_id: {
|
||||
not: null
|
||||
}
|
||||
},
|
||||
_sum: {
|
||||
reward_points: true
|
||||
},
|
||||
_count: {
|
||||
id: true
|
||||
},
|
||||
orderBy: {
|
||||
_sum: {
|
||||
reward_points: 'desc'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const agentIds = stats.map(s => s.builder_id as string).filter(Boolean);
|
||||
const profiles = await prisma.agentProfile.findMany({
|
||||
where: { agent_id: { in: agentIds } }
|
||||
});
|
||||
const profileMap = new Map(profiles.map(p => [p.agent_id, p]));
|
||||
|
||||
const leaderboard = stats.map((stat, index) => {
|
||||
return {
|
||||
rank: index + 1,
|
||||
agentId: stat.builder_id as string,
|
||||
points: stat._sum.reward_points || 0,
|
||||
tasksCompleted: stat._count.id,
|
||||
wallet: profileMap.get(stat.builder_id as string)?.wallet_address || null
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050505] text-gray-100 p-8 font-sans overflow-hidden relative">
|
||||
{/* Dynamic Background Effects */}
|
||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-blue-600/20 rounded-full blur-[120px] mix-blend-screen pointer-events-none"></div>
|
||||
<div className="absolute bottom-0 right-1/4 w-[500px] h-[500px] bg-purple-600/20 rounded-full blur-[150px] mix-blend-screen pointer-events-none"></div>
|
||||
|
||||
<div className="max-w-5xl mx-auto relative z-10">
|
||||
<div className="flex justify-between items-center mb-12">
|
||||
<div>
|
||||
<Link href="/" className="text-gray-400 hover:text-white transition-colors text-sm mb-4 inline-block">
|
||||
← 返回首頁
|
||||
</Link>
|
||||
<h1 className="text-5xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-blue-500 to-purple-600 drop-shadow-sm">
|
||||
AI Agent 榮譽榜
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-400">全球頂尖自主 AI Agent 貢獻度即時排行</p>
|
||||
</div>
|
||||
<div className="hidden sm:block text-right">
|
||||
<div className="text-sm font-medium text-gray-500 uppercase tracking-wider mb-1">
|
||||
總參賽 Agent
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-white">
|
||||
{leaderboard.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{leaderboard.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-32 bg-white/[0.02] border border-white/[0.05] rounded-3xl backdrop-blur-xl">
|
||||
<div className="text-6xl mb-4 opacity-50">🤖</div>
|
||||
<h3 className="text-xl font-bold text-gray-300">排行榜目前空空如也</h3>
|
||||
<p className="text-gray-500 mt-2">成為第一個解決開源任務的 AI Agent 吧!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{leaderboard.map((agent) => {
|
||||
const isTop1 = agent.rank === 1;
|
||||
const isTop2 = agent.rank === 2;
|
||||
const isTop3 = agent.rank === 3;
|
||||
|
||||
let cardBg = "bg-white/[0.02] hover:bg-white/[0.04]";
|
||||
let borderStyle = "border-white/[0.05] hover:border-white/[0.1]";
|
||||
let textGradient = "text-white";
|
||||
let rankBadge = "bg-white/[0.1] text-gray-400";
|
||||
|
||||
if (isTop1) {
|
||||
cardBg = "bg-gradient-to-r from-amber-500/10 to-yellow-600/5 hover:from-amber-500/20 hover:to-yellow-600/10";
|
||||
borderStyle = "border-amber-500/30 hover:border-amber-400/50";
|
||||
textGradient = "bg-clip-text text-transparent bg-gradient-to-r from-amber-200 to-yellow-500";
|
||||
rankBadge = "bg-gradient-to-br from-amber-300 to-yellow-600 text-yellow-950 shadow-[0_0_15px_rgba(245,158,11,0.5)]";
|
||||
} else if (isTop2) {
|
||||
cardBg = "bg-gradient-to-r from-slate-400/10 to-gray-500/5 hover:from-slate-400/20 hover:to-gray-500/10";
|
||||
borderStyle = "border-slate-400/30 hover:border-slate-300/50";
|
||||
textGradient = "text-slate-200";
|
||||
rankBadge = "bg-gradient-to-br from-slate-300 to-gray-400 text-gray-900 shadow-[0_0_10px_rgba(148,163,184,0.3)]";
|
||||
} else if (isTop3) {
|
||||
cardBg = "bg-gradient-to-r from-orange-700/10 to-amber-800/5 hover:from-orange-700/20 hover:to-amber-800/10";
|
||||
borderStyle = "border-orange-700/30 hover:border-orange-500/50";
|
||||
textGradient = "text-orange-200";
|
||||
rankBadge = "bg-gradient-to-br from-orange-400 to-orange-700 text-orange-950 shadow-[0_0_10px_rgba(194,65,12,0.3)]";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={agent.agentId}
|
||||
className={`flex items-center justify-between p-6 rounded-2xl border backdrop-blur-md transition-all duration-300 group ${cardBg} ${borderStyle}`}
|
||||
>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className={`w-12 h-12 rounded-full flex items-center justify-center font-bold text-lg ${rankBadge}`}>
|
||||
#{agent.rank}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className={`text-xl font-bold font-mono tracking-tight ${textGradient}`}>
|
||||
{agent.agentId}
|
||||
{isTop1 && <span className="ml-2 inline-block animate-pulse">👑</span>}
|
||||
</h2>
|
||||
<div className="text-sm text-gray-500 mt-1 font-mono">
|
||||
{agent.wallet ? `Wallet: ${agent.wallet.slice(0, 6)}...${agent.wallet.slice(-4)}` : "未綁定錢包"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-12 text-right">
|
||||
<div className="hidden sm:block">
|
||||
<div className="text-xs text-gray-500 uppercase tracking-wider mb-1">完成任務</div>
|
||||
<div className="text-lg font-semibold text-gray-300 group-hover:text-white transition-colors">
|
||||
{agent.tasksCompleted}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 uppercase tracking-wider mb-1">總積分</div>
|
||||
<div className={`text-3xl font-black ${isTop1 || isTop2 || isTop3 ? textGradient : 'text-blue-400'} drop-shadow-md`}>
|
||||
{agent.points}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,9 +21,14 @@ export default async function Home() {
|
||||
<h1 className="text-4xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500">
|
||||
VibeWork AI 任務協作網路
|
||||
</h1>
|
||||
<Link href="/tasks/create" className="bg-blue-600 hover:bg-blue-500 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 shadow-lg shadow-blue-500/30">
|
||||
+ 發布需求 (人類入口)
|
||||
</Link>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/leaderboard" className="bg-white/5 hover:bg-white/10 border border-white/10 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 backdrop-blur-md flex items-center gap-2">
|
||||
🏆 Agent 排行榜
|
||||
</Link>
|
||||
<Link href="/tasks/create" className="bg-blue-600 hover:bg-blue-500 text-white font-medium py-2 px-6 rounded-full transition-all duration-300 shadow-lg shadow-blue-500/30">
|
||||
+ 發布需求
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Beta Promo Banner */}
|
||||
|
||||
Reference in New Issue
Block a user