veza/apps/web/src/components/gamification/LeaderboardView.tsx

132 lines
7.2 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { LeaderboardEntry } from '../../types';
import { ChevronUp, ChevronDown, Minus, Crown, Loader2 } from 'lucide-react';
import { gamificationService } from '../../services/gamificationService';
import { logger } from '@/utils/logger';
export const LeaderboardView: React.FC = () => {
const [period, setPeriod] = useState<'weekly' | 'monthly' | 'all'>('weekly');
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadLeaderboard = async () => {
setLoading(true);
try {
const data = await gamificationService.getLeaderboard(period);
setLeaderboard(data);
} catch (e) {
logger.error('Error loading leaderboard', {
error: e instanceof Error ? e.message : String(e),
stack: e instanceof Error ? e.stack : undefined,
period,
});
} finally {
setLoading(false);
}
};
loadLeaderboard();
}, [period]);
return (
<div className="space-y-8 animate-fadeIn pb-20 max-w-5xl mx-auto">
{/* Header */}
<div className="flex flex-col md:flex-row justify-between items-end border-b border-kodo-steel/50 pb-6 gap-4">
<div>
<h2 className="text-3xl font-display font-bold text-white mb-2">LEADERBOARD</h2>
<p className="text-gray-400 font-mono text-sm">Top producers dominating the network.</p>
</div>
<div className="flex bg-kodo-ink p-1 rounded-lg border border-kodo-steel">
{['weekly', 'monthly', 'all'].map(p => (
<button
key={p}
onClick={() => setPeriod(p as any)}
className={`px-4 py-2 rounded text-xs font-bold uppercase transition-all ${period === p ? 'bg-kodo-gold text-black shadow-lg' : 'text-gray-400 hover:text-white'}`}
>
{p === 'all' ? 'All Time' : p}
</button>
))}
</div>
</div>
{loading ? (
<div className="flex justify-center py-20"><Loader2 className="w-10 h-10 text-kodo-cyan animate-spin" /></div>
) : (
<>
{/* Top 3 Podium (Visual) */}
{leaderboard.length >= 3 && (
<div className="grid grid-cols-3 gap-4 items-end mb-8 md:px-20">
{[leaderboard[1], leaderboard[0], leaderboard[2]].map((entry, i) => (
<div key={entry.userId} className={`flex flex-col items-center ${i === 1 ? '-mt-12 order-2' : i === 0 ? 'order-1' : 'order-3'}`}>
<div className="relative mb-4">
<div className={`w-20 h-20 md:w-24 md:h-24 rounded-full overflow-hidden border-4 ${i === 1 ? 'border-kodo-gold' : i === 0 ? 'border-gray-300' : 'border-orange-400'}`}>
<img src={entry.avatar} className="w-full h-full object-cover" />
</div>
{i === 1 && <Crown className="absolute -top-8 left-1/2 -translate-x-1/2 w-10 h-10 text-kodo-gold fill-current animate-bounce" />}
<div className="absolute -bottom-3 left-1/2 -translate-x-1/2 bg-black px-2 py-0.5 rounded-full text-xs font-bold border border-white/20">
{entry.rank}
</div>
</div>
<div className="text-center">
<div className="font-bold text-white text-lg">{entry.username}</div>
<div className="text-xs text-kodo-cyan">{entry.xp.toLocaleString()} XP</div>
</div>
</div>
))}
</div>
)}
{/* Table */}
<Card variant="default" className="p-0 overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="border-b border-kodo-steel bg-kodo-ink text-xs font-bold text-gray-500 uppercase tracking-wider">
<th className="p-4 w-16 text-center">Rank</th>
<th className="p-4">Producer</th>
<th className="p-4">Level</th>
<th className="p-4 text-right">XP</th>
<th className="p-4 text-center">Trend</th>
</tr>
</thead>
<tbody className="divide-y divide-kodo-steel/30 text-sm">
{leaderboard.map(entry => (
<tr key={entry.userId} className="hover:bg-white/5 transition-colors group">
<td className="p-4 text-center font-bold font-mono text-gray-400">
#{entry.rank}
</td>
<td className="p-4">
<div className="flex items-center gap-3">
<img src={entry.avatar} className="w-8 h-8 rounded-full" />
<span className="font-bold text-white group-hover:text-kodo-cyan transition-colors">{entry.username}</span>
</div>
</td>
<td className="p-4">
<span className="bg-kodo-slate px-2 py-1 rounded text-xs font-mono text-gray-300">LVL {entry.level}</span>
</td>
<td className="p-4 text-right font-mono font-bold text-white">
{entry.xp.toLocaleString()}
</td>
<td className="p-4 text-center">
{entry.trend > 0 ? (
<span className="text-kodo-lime flex items-center justify-center gap-1"><ChevronUp className="w-4 h-4" /> {entry.trend}</span>
) : entry.trend < 0 ? (
<span className="text-kodo-red flex items-center justify-center gap-1"><ChevronDown className="w-4 h-4" /> {Math.abs(entry.trend)}</span>
) : (
<span className="text-gray-500 flex items-center justify-center"><Minus className="w-4 h-4" /></span>
)}
</td>
</tr>
))}
</tbody>
</table>
</Card>
</>
)}
</div>
);
};