import { useState, useEffect, useRef } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' import { marathonsApi, eventsApi, challengesApi } from '@/api' import type { Marathon, ActiveEvent, Challenge } from '@/types' import { NeonButton, GlassCard, StatsCard } from '@/components/ui' import { useAuthStore } from '@/store/auth' import { useToast } from '@/store/toast' import { useConfirm } from '@/store/confirm' import { EventBanner } from '@/components/EventBanner' import { EventControl } from '@/components/EventControl' import { ActivityFeed, type ActivityFeedRef } from '@/components/ActivityFeed' import { Users, Calendar, Trophy, Play, Settings, Copy, Check, Loader2, Trash2, Globe, Lock, CalendarCheck, UserPlus, Gamepad2, ArrowLeft, Zap, Flag, Star, Target, TrendingDown, ChevronDown, ChevronUp, Link2, Sparkles } from 'lucide-react' import { format } from 'date-fns' import { ru } from 'date-fns/locale' import { TelegramBotBanner } from '@/components/TelegramBotBanner' export function MarathonPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const user = useAuthStore((state) => state.user) const toast = useToast() const confirm = useConfirm() const [marathon, setMarathon] = useState(null) const [activeEvent, setActiveEvent] = useState(null) const [challenges, setChallenges] = useState([]) const [isLoading, setIsLoading] = useState(true) const [copied, setCopied] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isJoining, setIsJoining] = useState(false) const [isFinishing, setIsFinishing] = useState(false) const [showEventControl, setShowEventControl] = useState(false) const [showChallenges, setShowChallenges] = useState(false) const [expandedGameId, setExpandedGameId] = useState(null) const activityFeedRef = useRef(null) useEffect(() => { loadMarathon() }, [id]) const loadMarathon = async () => { if (!id) return try { const data = await marathonsApi.get(parseInt(id)) setMarathon(data) if (data.status === 'active' && data.my_participation) { const eventData = await eventsApi.getActive(parseInt(id)) setActiveEvent(eventData) // Load challenges for all participants try { const challengesData = await challengesApi.list(parseInt(id)) setChallenges(challengesData) } catch { // Ignore if no challenges } } } catch (error) { console.error('Failed to load marathon:', error) navigate('/marathons') } finally { setIsLoading(false) } } const refreshEvent = async () => { if (!id) return try { const eventData = await eventsApi.getActive(parseInt(id)) setActiveEvent(eventData) activityFeedRef.current?.refresh() } catch (error) { console.error('Failed to refresh event:', error) } } const getInviteLink = () => { if (!marathon) return '' return `${window.location.origin}/invite/${marathon.invite_code}` } const copyInviteLink = () => { if (marathon) { navigator.clipboard.writeText(getInviteLink()) setCopied(true) setTimeout(() => setCopied(false), 2000) } } const handleDelete = async () => { if (!marathon) return const confirmed = await confirm({ title: 'Удалить марафон?', message: 'Все данные марафона будут удалены безвозвратно.', confirmText: 'Удалить', cancelText: 'Отмена', variant: 'danger', }) if (!confirmed) return setIsDeleting(true) try { await marathonsApi.delete(marathon.id) navigate('/marathons') } catch (error) { console.error('Failed to delete marathon:', error) toast.error('Не удалось удалить марафон') } finally { setIsDeleting(false) } } const handleJoinPublic = async () => { if (!marathon) return setIsJoining(true) try { const updated = await marathonsApi.joinPublic(marathon.id) setMarathon(updated) } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось присоединиться') } finally { setIsJoining(false) } } const handleFinish = async () => { if (!marathon) return const confirmed = await confirm({ title: 'Завершить марафон?', message: 'Марафон будет завершён досрочно. Участники больше не смогут выполнять задания.', confirmText: 'Завершить', cancelText: 'Отмена', variant: 'warning', }) if (!confirmed) return setIsFinishing(true) try { const updated = await marathonsApi.finish(marathon.id) setMarathon(updated) toast.success('Марафон завершён') } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось завершить марафон') } finally { setIsFinishing(false) } } if (isLoading || !marathon) { return (

Загрузка марафона...

) } const isOrganizer = marathon.my_participation?.role === 'organizer' || user?.role === 'admin' const isParticipant = !!marathon.my_participation const isCreator = marathon.creator.id === user?.id const canDelete = isCreator || user?.role === 'admin' const statusConfig = { active: { color: 'text-neon-400', bg: 'bg-neon-500/20', border: 'border-neon-500/30', label: 'Активен' }, preparing: { color: 'text-yellow-400', bg: 'bg-yellow-500/20', border: 'border-yellow-500/30', label: 'Подготовка' }, finished: { color: 'text-gray-400', bg: 'bg-gray-500/20', border: 'border-gray-500/30', label: 'Завершён' }, } const status = statusConfig[marathon.status as keyof typeof statusConfig] || statusConfig.finished return (
{/* Back button */} К списку марафонов {/* Hero Banner */}
{/* Background */}
{/* Title & Description */}

{marathon.title}

{marathon.is_public ? : } {marathon.is_public ? 'Открытый' : 'Закрытый'} {status.label}
{marathon.description && (

{marathon.description}

)}
{/* Action Buttons */}
{marathon.is_public && !isParticipant && marathon.status !== 'finished' && ( }> Присоединиться )} {marathon.status === 'preparing' && isOrganizer && ( }> Настройка )} {marathon.status === 'preparing' && isParticipant && !isOrganizer && marathon.game_proposal_mode === 'all_participants' && ( }> Предложить игру )} {marathon.status === 'active' && isParticipant && ( }> Играть )} }> Рейтинг {marathon.status === 'active' && isOrganizer && ( )} {canDelete && ( } /> )}
{/* Main content */}
{/* Stats */}
} color="neon" /> } color="purple" /> } color="default" /> } color="default" /> } color={marathon.status === 'active' ? 'neon' : marathon.status === 'preparing' ? 'default' : 'default'} />
{/* Telegram Bot Banner */} {/* Active event banner */} {marathon.status === 'active' && activeEvent?.event && ( )} {/* Event control for organizers */} {marathon.status === 'active' && isOrganizer && ( {showEventControl && activeEvent && (
)}
)} {/* Invite link */} {marathon.status !== 'finished' && (

Пригласить друзей

Поделитесь ссылкой

{getInviteLink()} : }> {copied ? 'Скопировано!' : 'Копировать'}
)} {/* My stats */} {marathon.my_participation && (

Ваша статистика

{marathon.my_participation.total_points}
Очков
{marathon.my_participation.current_streak} {marathon.my_participation.current_streak > 0 && ( 🔥 )}
Серия
{marathon.my_participation.drop_count}
Пропусков
)} {/* All challenges viewer */} {marathon.status === 'active' && isParticipant && challenges.length > 0 && ( {showChallenges && (
{/* Group challenges by game */} {Array.from(new Set(challenges.map(c => c.game.id))).map(gameId => { const gameChallenges = challenges.filter(c => c.game.id === gameId) const game = gameChallenges[0]?.game if (!game) return null const isExpanded = expandedGameId === gameId return (
{isExpanded && (
{gameChallenges.map(challenge => (
{challenge.difficulty === 'easy' ? 'Легко' : challenge.difficulty === 'medium' ? 'Средне' : 'Сложно'} +{challenge.points} {challenge.type === 'completion' ? 'Прохождение' : challenge.type === 'no_death' ? 'Без смертей' : challenge.type === 'speedrun' ? 'Спидран' : challenge.type === 'collection' ? 'Коллекция' : challenge.type === 'achievement' ? 'Достижение' : 'Челлендж-ран'}
{challenge.title}

{challenge.description}

{challenge.proof_hint && (

Пруф: {challenge.proof_hint}

)}
))}
)}
) })}
)}
)}
{/* Activity Feed - right sidebar */} {isParticipant && (
)}
) }