120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import { useState, useEffect } from 'react'
|
||
import { useParams, Link } from 'react-router-dom'
|
||
import { marathonsApi } from '@/api'
|
||
import type { LeaderboardEntry } from '@/types'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'
|
||
import { useAuthStore } from '@/store/auth'
|
||
import { Trophy, Flame, ArrowLeft, Loader2 } from 'lucide-react'
|
||
|
||
export function LeaderboardPage() {
|
||
const { id } = useParams<{ id: string }>()
|
||
const user = useAuthStore((state) => state.user)
|
||
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([])
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
|
||
useEffect(() => {
|
||
loadLeaderboard()
|
||
}, [id])
|
||
|
||
const loadLeaderboard = async () => {
|
||
if (!id) return
|
||
try {
|
||
const data = await marathonsApi.getLeaderboard(parseInt(id))
|
||
setLeaderboard(data)
|
||
} catch (error) {
|
||
console.error('Failed to load leaderboard:', error)
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
const getRankIcon = (rank: number) => {
|
||
switch (rank) {
|
||
case 1:
|
||
return <Trophy className="w-6 h-6 text-yellow-500" />
|
||
case 2:
|
||
return <Trophy className="w-6 h-6 text-gray-400" />
|
||
case 3:
|
||
return <Trophy className="w-6 h-6 text-amber-700" />
|
||
default:
|
||
return <span className="text-gray-500 font-mono w-6 text-center">{rank}</span>
|
||
}
|
||
}
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex justify-center py-12">
|
||
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-2xl mx-auto">
|
||
<div className="flex items-center gap-4 mb-8">
|
||
<Link to={`/marathons/${id}`} className="text-gray-400 hover:text-white">
|
||
<ArrowLeft className="w-6 h-6" />
|
||
</Link>
|
||
<h1 className="text-2xl font-bold text-white">Таблица лидеров</h1>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Trophy className="w-5 h-5 text-yellow-500" />
|
||
Рейтинг
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{leaderboard.length === 0 ? (
|
||
<p className="text-center text-gray-400 py-8">Пока нет участников</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{leaderboard.map((entry) => (
|
||
<div
|
||
key={entry.user.id}
|
||
className={`flex items-center gap-4 p-4 rounded-lg ${
|
||
entry.user.id === user?.id
|
||
? 'bg-primary-500/20 border border-primary-500/50'
|
||
: 'bg-gray-900'
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-center w-8">
|
||
{getRankIcon(entry.rank)}
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<div className="font-medium text-white">
|
||
{entry.user.nickname}
|
||
{entry.user.id === user?.id && (
|
||
<span className="ml-2 text-xs text-primary-400">(Вы)</span>
|
||
)}
|
||
</div>
|
||
<div className="text-sm text-gray-400">
|
||
{entry.completed_count} выполнено, {entry.dropped_count} пропущено
|
||
</div>
|
||
</div>
|
||
|
||
{entry.current_streak > 0 && (
|
||
<div className="flex items-center gap-1 text-yellow-500">
|
||
<Flame className="w-4 h-4" />
|
||
<span className="text-sm">{entry.current_streak}</span>
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-right">
|
||
<div className="text-xl font-bold text-primary-400">
|
||
{entry.total_points}
|
||
</div>
|
||
<div className="text-xs text-gray-500">очков</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|