198 lines
6.7 KiB
TypeScript
198 lines
6.7 KiB
TypeScript
|
|
import { useState, useEffect } from 'react'
|
|||
|
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
|||
|
|
import { marathonsApi } from '@/api'
|
|||
|
|
import type { Marathon } from '@/types'
|
|||
|
|
import { Button, Card, CardContent } from '@/components/ui'
|
|||
|
|
import { useAuthStore } from '@/store/auth'
|
|||
|
|
import { Users, Calendar, Trophy, Play, Settings, Copy, Check, Loader2 } from 'lucide-react'
|
|||
|
|
import { format } from 'date-fns'
|
|||
|
|
|
|||
|
|
export function MarathonPage() {
|
|||
|
|
const { id } = useParams<{ id: string }>()
|
|||
|
|
const navigate = useNavigate()
|
|||
|
|
const user = useAuthStore((state) => state.user)
|
|||
|
|
const [marathon, setMarathon] = useState<Marathon | null>(null)
|
|||
|
|
const [isLoading, setIsLoading] = useState(true)
|
|||
|
|
const [copied, setCopied] = useState(false)
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
loadMarathon()
|
|||
|
|
}, [id])
|
|||
|
|
|
|||
|
|
const loadMarathon = async () => {
|
|||
|
|
if (!id) return
|
|||
|
|
try {
|
|||
|
|
const data = await marathonsApi.get(parseInt(id))
|
|||
|
|
setMarathon(data)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to load marathon:', error)
|
|||
|
|
navigate('/marathons')
|
|||
|
|
} finally {
|
|||
|
|
setIsLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const copyInviteCode = () => {
|
|||
|
|
if (marathon) {
|
|||
|
|
navigator.clipboard.writeText(marathon.invite_code)
|
|||
|
|
setCopied(true)
|
|||
|
|
setTimeout(() => setCopied(false), 2000)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isLoading || !marathon) {
|
|||
|
|
return (
|
|||
|
|
<div className="flex justify-center py-12">
|
|||
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isOrganizer = user?.id === marathon.organizer.id
|
|||
|
|
const isParticipant = !!marathon.my_participation
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="max-w-4xl mx-auto">
|
|||
|
|
{/* Header */}
|
|||
|
|
<div className="flex justify-between items-start mb-8">
|
|||
|
|
<div>
|
|||
|
|
<h1 className="text-3xl font-bold text-white mb-2">{marathon.title}</h1>
|
|||
|
|
{marathon.description && (
|
|||
|
|
<p className="text-gray-400">{marathon.description}</p>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex gap-2">
|
|||
|
|
{marathon.status === 'preparing' && isOrganizer && (
|
|||
|
|
<Link to={`/marathons/${id}/lobby`}>
|
|||
|
|
<Button variant="secondary">
|
|||
|
|
<Settings className="w-4 h-4 mr-2" />
|
|||
|
|
Настройка
|
|||
|
|
</Button>
|
|||
|
|
</Link>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{marathon.status === 'active' && isParticipant && (
|
|||
|
|
<Link to={`/marathons/${id}/play`}>
|
|||
|
|
<Button>
|
|||
|
|
<Play className="w-4 h-4 mr-2" />
|
|||
|
|
Играть
|
|||
|
|
</Button>
|
|||
|
|
</Link>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Link to={`/marathons/${id}/leaderboard`}>
|
|||
|
|
<Button variant="secondary">
|
|||
|
|
<Trophy className="w-4 h-4 mr-2" />
|
|||
|
|
Рейтинг
|
|||
|
|
</Button>
|
|||
|
|
</Link>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Stats */}
|
|||
|
|
<div className="grid md:grid-cols-4 gap-4 mb-8">
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="text-center py-4">
|
|||
|
|
<div className="text-2xl font-bold text-white">{marathon.participants_count}</div>
|
|||
|
|
<div className="text-sm text-gray-400 flex items-center justify-center gap-1">
|
|||
|
|
<Users className="w-4 h-4" />
|
|||
|
|
Участников
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="text-center py-4">
|
|||
|
|
<div className="text-2xl font-bold text-white">{marathon.games_count}</div>
|
|||
|
|
<div className="text-sm text-gray-400">Игр</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="text-center py-4">
|
|||
|
|
<div className="text-2xl font-bold text-white">
|
|||
|
|
{marathon.start_date ? format(new Date(marathon.start_date), 'd MMM') : '-'}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-gray-400 flex items-center justify-center gap-1">
|
|||
|
|
<Calendar className="w-4 h-4" />
|
|||
|
|
Дата начала
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card>
|
|||
|
|
<CardContent className="text-center py-4">
|
|||
|
|
<div className={`text-2xl font-bold ${
|
|||
|
|
marathon.status === 'active' ? 'text-green-500' :
|
|||
|
|
marathon.status === 'preparing' ? 'text-yellow-500' : 'text-gray-400'
|
|||
|
|
}`}>
|
|||
|
|
{marathon.status === 'active' ? 'Активен' : marathon.status === 'preparing' ? 'Подготовка' : 'Завершён'}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-gray-400">Статус</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Invite code */}
|
|||
|
|
{marathon.status !== 'finished' && (
|
|||
|
|
<Card className="mb-8">
|
|||
|
|
<CardContent>
|
|||
|
|
<h3 className="font-medium text-white mb-3">Код приглашения</h3>
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
<code className="flex-1 px-4 py-2 bg-gray-900 rounded-lg text-primary-400 font-mono">
|
|||
|
|
{marathon.invite_code}
|
|||
|
|
</code>
|
|||
|
|
<Button variant="secondary" onClick={copyInviteCode}>
|
|||
|
|
{copied ? (
|
|||
|
|
<>
|
|||
|
|
<Check className="w-4 h-4 mr-2" />
|
|||
|
|
Скопировано!
|
|||
|
|
</>
|
|||
|
|
) : (
|
|||
|
|
<>
|
|||
|
|
<Copy className="w-4 h-4 mr-2" />
|
|||
|
|
Копировать
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
<p className="text-sm text-gray-500 mt-2">
|
|||
|
|
Поделитесь этим кодом с друзьями, чтобы они могли присоединиться к марафону
|
|||
|
|
</p>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* My stats */}
|
|||
|
|
{marathon.my_participation && (
|
|||
|
|
<Card>
|
|||
|
|
<CardContent>
|
|||
|
|
<h3 className="font-medium text-white mb-4">Ваша статистика</h3>
|
|||
|
|
<div className="grid grid-cols-3 gap-4 text-center">
|
|||
|
|
<div>
|
|||
|
|
<div className="text-2xl font-bold text-primary-500">
|
|||
|
|
{marathon.my_participation.total_points}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-gray-400">Очков</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div className="text-2xl font-bold text-yellow-500">
|
|||
|
|
{marathon.my_participation.current_streak}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-gray-400">Серия</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div className="text-2xl font-bold text-gray-400">
|
|||
|
|
{marathon.my_participation.drop_count}
|
|||
|
|
</div>
|
|||
|
|
<div className="text-sm text-gray-400">Пропусков</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|