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, MarathonDispute } 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 { MarathonSettingsModal } from '@/components/MarathonSettingsModal' 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, AlertTriangle, Clock, CheckCircle, XCircle, ThumbsUp, ThumbsDown, ExternalLink, User } 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 [showSettings, setShowSettings] = useState(false) const activityFeedRef = useRef(null) // Disputes for organizers const [showDisputes, setShowDisputes] = useState(false) const [disputes, setDisputes] = useState([]) const [loadingDisputes, setLoadingDisputes] = useState(false) const [disputeFilter, setDisputeFilter] = useState<'open' | 'all'>('open') const [resolvingDisputeId, setResolvingDisputeId] = useState(null) useEffect(() => { loadMarathon() }, [id]) useEffect(() => { if (showDisputes) { loadDisputes() } }, [showDisputes, disputeFilter]) 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 loadDisputes = async () => { if (!id) return setLoadingDisputes(true) try { const data = await marathonsApi.listDisputes(parseInt(id), disputeFilter) setDisputes(data) } catch (error) { console.error('Failed to load disputes:', error) toast.error('Не удалось загрузить оспаривания') } finally { setLoadingDisputes(false) } } const handleResolveDispute = async (disputeId: number, isValid: boolean) => { if (!id) return setResolvingDisputeId(disputeId) try { await marathonsApi.resolveDispute(parseInt(id), disputeId, isValid) toast.success(isValid ? 'Пруф подтверждён' : 'Пруф отклонён') await loadDisputes() } catch (error) { console.error('Failed to resolve dispute:', error) toast.error('Не удалось разрешить диспут') } finally { setResolvingDisputeId(null) } } const formatDisputeDate = (dateString: string) => { return new Date(dateString).toLocaleString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', }) } const getDisputeTimeRemaining = (expiresAt: string) => { const now = new Date() const expires = new Date(expiresAt) const diff = expires.getTime() - now.getTime() if (diff <= 0) return 'Истекло' const hours = Math.floor(diff / (1000 * 60 * 60)) const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) return `${hours}ч ${minutes}м` } 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 */} {marathon.cover_url ? ( <>
) : ( <>
)}
{/* 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 && ( )} {marathon.status === 'preparing' && isOrganizer && ( setShowSettings(true)} className="!text-gray-400 hover:!bg-dark-600" icon={} /> )} {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 && (
)}
)} {/* Disputes management for organizers */} {marathon.status === 'active' && isOrganizer && ( {showDisputes && (
{/* Filters */}
{/* Loading */} {loadingDisputes ? (
) : disputes.length === 0 ? (

{disputeFilter === 'open' ? 'Нет открытых оспариваний' : 'Нет оспариваний'}

) : (
{disputes.map((dispute) => (
{/* Challenge title */}

{dispute.challenge_title}

{/* Participants */}
Автор: {dispute.participant_nickname} Оспорил: {dispute.raised_by_nickname}
{/* Reason */}

{dispute.reason}

{/* Votes & Time */}
{dispute.votes_valid}
/
{dispute.votes_invalid}
{formatDisputeDate(dispute.created_at)} {dispute.status === 'open' && ( {getDisputeTimeRemaining(dispute.expires_at)} )}
{/* Right side - Status & Actions */}
{dispute.status === 'open' ? ( Открыт ) : dispute.status === 'valid' ? ( Валидно ) : ( Невалидно )} {/* Link to assignment */} {dispute.assignment_id && ( Открыть )} {/* Resolution buttons */} {dispute.status === 'open' && (
handleResolveDispute(dispute.id, true)} isLoading={resolvingDisputeId === dispute.id} disabled={resolvingDisputeId !== null} icon={} > Валидно handleResolveDispute(dispute.id, false)} isLoading={resolvingDisputeId === dispute.id} disabled={resolvingDisputeId !== null} icon={} > Невалидно
)}
))}
)}
)}
)} {/* 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 && (
)}
{/* Settings Modal */} setShowSettings(false)} onUpdate={setMarathon} />
) }