This commit is contained in:
2025-12-14 02:38:35 +07:00
commit 5343a8f2c3
84 changed files with 7406 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
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>
)
}