import { useState, useEffect, useRef } from 'react' import { useParams, Link } from 'react-router-dom' import { marathonsApi, wheelApi, gamesApi, eventsApi } from '@/api' import type { Marathon, Assignment, SpinResult, Game, ActiveEvent, DroppedAssignment, SwapCandidate, MySwapRequests, CommonEnemyLeaderboardEntry, EventAssignment } from '@/types' import { Button, Card, CardContent } from '@/components/ui' import { SpinWheel } from '@/components/SpinWheel' import { EventBanner } from '@/components/EventBanner' import { Loader2, Upload, X, RotateCcw, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft } from 'lucide-react' export function PlayPage() { const { id } = useParams<{ id: string }>() const [marathon, setMarathon] = useState(null) const [currentAssignment, setCurrentAssignment] = useState(null) const [spinResult, setSpinResult] = useState(null) const [games, setGames] = useState([]) const [activeEvent, setActiveEvent] = useState(null) const [isLoading, setIsLoading] = useState(true) // Complete state const [proofFile, setProofFile] = useState(null) const [proofUrl, setProofUrl] = useState('') const [comment, setComment] = useState('') const [isCompleting, setIsCompleting] = useState(false) // Drop state const [isDropping, setIsDropping] = useState(false) // Rematch state const [droppedAssignments, setDroppedAssignments] = useState([]) const [isRematchLoading, setIsRematchLoading] = useState(false) const [rematchingId, setRematchingId] = useState(null) // Swap state const [swapCandidates, setSwapCandidates] = useState([]) const [swapRequests, setSwapRequests] = useState({ incoming: [], outgoing: [] }) const [isSwapLoading, setIsSwapLoading] = useState(false) const [sendingRequestTo, setSendingRequestTo] = useState(null) const [processingRequestId, setProcessingRequestId] = useState(null) // Common Enemy leaderboard state const [commonEnemyLeaderboard, setCommonEnemyLeaderboard] = useState([]) // Tab state for Common Enemy type PlayTab = 'spin' | 'event' const [activeTab, setActiveTab] = useState('spin') // Event assignment state (Common Enemy) const [eventAssignment, setEventAssignment] = useState(null) const [eventProofFile, setEventProofFile] = useState(null) const [eventProofUrl, setEventProofUrl] = useState('') const [eventComment, setEventComment] = useState('') const [isEventCompleting, setIsEventCompleting] = useState(false) const fileInputRef = useRef(null) const eventFileInputRef = useRef(null) useEffect(() => { loadData() }, [id]) // Load dropped assignments when rematch event is active useEffect(() => { if (activeEvent?.event?.type === 'rematch' && !currentAssignment) { loadDroppedAssignments() } }, [activeEvent?.event?.type, currentAssignment]) // Load swap candidates and requests when swap event is active useEffect(() => { if (activeEvent?.event?.type === 'swap') { loadSwapRequests() if (currentAssignment) { loadSwapCandidates() } } }, [activeEvent?.event?.type, currentAssignment]) // Load common enemy leaderboard when common_enemy event is active useEffect(() => { if (activeEvent?.event?.type === 'common_enemy') { loadCommonEnemyLeaderboard() // Poll for updates every 10 seconds const interval = setInterval(loadCommonEnemyLeaderboard, 10000) return () => clearInterval(interval) } }, [activeEvent?.event?.type]) const loadDroppedAssignments = async () => { if (!id) return setIsRematchLoading(true) try { const dropped = await eventsApi.getDroppedAssignments(parseInt(id)) setDroppedAssignments(dropped) } catch (error) { console.error('Failed to load dropped assignments:', error) } finally { setIsRematchLoading(false) } } const loadSwapCandidates = async () => { if (!id) return setIsSwapLoading(true) try { const candidates = await eventsApi.getSwapCandidates(parseInt(id)) setSwapCandidates(candidates) } catch (error) { console.error('Failed to load swap candidates:', error) } finally { setIsSwapLoading(false) } } const loadSwapRequests = async () => { if (!id) return try { const requests = await eventsApi.getSwapRequests(parseInt(id)) setSwapRequests(requests) } catch (error) { console.error('Failed to load swap requests:', error) } } const loadCommonEnemyLeaderboard = async () => { if (!id) return try { const leaderboard = await eventsApi.getCommonEnemyLeaderboard(parseInt(id)) setCommonEnemyLeaderboard(leaderboard) } catch (error) { console.error('Failed to load common enemy leaderboard:', error) } } const loadData = async () => { if (!id) return try { const [marathonData, assignment, gamesData, eventData, eventAssignmentData] = await Promise.all([ marathonsApi.get(parseInt(id)), wheelApi.getCurrentAssignment(parseInt(id)), gamesApi.list(parseInt(id), 'approved'), eventsApi.getActive(parseInt(id)), eventsApi.getEventAssignment(parseInt(id)), ]) setMarathon(marathonData) setCurrentAssignment(assignment) setGames(gamesData) setActiveEvent(eventData) setEventAssignment(eventAssignmentData) } catch (error) { console.error('Failed to load data:', error) } finally { setIsLoading(false) } } const refreshEvent = async () => { if (!id) return try { const eventData = await eventsApi.getActive(parseInt(id)) setActiveEvent(eventData) } catch (error) { console.error('Failed to refresh event:', error) } } const handleSpin = async (): Promise => { if (!id) return null try { const result = await wheelApi.spin(parseInt(id)) setSpinResult(result) return result.game } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось крутить') return null } } const handleSpinComplete = async () => { // Small delay then reload data to show the assignment setTimeout(async () => { await loadData() }, 500) } const handleComplete = async () => { if (!currentAssignment) return if (!proofFile && !proofUrl) { alert('Пожалуйста, предоставьте доказательство (файл или ссылку)') return } setIsCompleting(true) try { const result = await wheelApi.complete(currentAssignment.id, { proof_file: proofFile || undefined, proof_url: proofUrl || undefined, comment: comment || undefined, }) alert(`Выполнено! +${result.points_earned} очков (бонус серии: +${result.streak_bonus})`) // Reset form setProofFile(null) setProofUrl('') setComment('') setSpinResult(null) await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось выполнить') } finally { setIsCompleting(false) } } const handleDrop = async () => { if (!currentAssignment) return const penalty = spinResult?.drop_penalty || 0 if (!confirm(`Пропустить это задание? Вы потеряете ${penalty} очков.`)) return setIsDropping(true) try { const result = await wheelApi.drop(currentAssignment.id) alert(`Пропущено. Штраф: -${result.penalty} очков`) setSpinResult(null) await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось пропустить') } finally { setIsDropping(false) } } const handleEventComplete = async () => { if (!eventAssignment?.assignment) return if (!eventProofFile && !eventProofUrl) { alert('Пожалуйста, предоставьте доказательство (файл или ссылку)') return } setIsEventCompleting(true) try { const result = await eventsApi.completeEventAssignment(eventAssignment.assignment.id, { proof_file: eventProofFile || undefined, proof_url: eventProofUrl || undefined, comment: eventComment || undefined, }) alert(`Выполнено! +${result.points_earned} очков`) // Reset form setEventProofFile(null) setEventProofUrl('') setEventComment('') await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось выполнить') } finally { setIsEventCompleting(false) } } const handleRematch = async (assignmentId: number) => { if (!id) return if (!confirm('Начать реванш? Вы получите 50% от обычных очков за выполнение.')) return setRematchingId(assignmentId) try { await eventsApi.rematch(parseInt(id), assignmentId) alert('Реванш начат! Выполните задание за 50% очков.') await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось начать реванш') } finally { setRematchingId(null) } } const handleSendSwapRequest = async (participantId: number, participantName: string, theirChallenge: string) => { if (!id) return if (!confirm(`Отправить запрос на обмен с ${participantName}?\n\nВы предлагаете обменяться на: "${theirChallenge}"\n\n${participantName} должен будет подтвердить обмен.`)) return setSendingRequestTo(participantId) try { await eventsApi.createSwapRequest(parseInt(id), participantId) alert('Запрос на обмен отправлен! Ожидайте подтверждения.') await loadSwapRequests() await loadSwapCandidates() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось отправить запрос') } finally { setSendingRequestTo(null) } } const handleAcceptSwapRequest = async (requestId: number) => { if (!id) return if (!confirm('Принять обмен? Задания будут обменяны сразу после подтверждения.')) return setProcessingRequestId(requestId) try { await eventsApi.acceptSwapRequest(parseInt(id), requestId) alert('Обмен выполнен!') await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось выполнить обмен') } finally { setProcessingRequestId(null) } } const handleDeclineSwapRequest = async (requestId: number) => { if (!id) return setProcessingRequestId(requestId) try { await eventsApi.declineSwapRequest(parseInt(id), requestId) await loadSwapRequests() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось отклонить запрос') } finally { setProcessingRequestId(null) } } const handleCancelSwapRequest = async (requestId: number) => { if (!id) return setProcessingRequestId(requestId) try { await eventsApi.cancelSwapRequest(parseInt(id), requestId) await loadSwapRequests() await loadSwapCandidates() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось отменить запрос') } finally { setProcessingRequestId(null) } } if (isLoading) { return (
) } if (!marathon) { return
Марафон не найден
} const participation = marathon.my_participation return (
{/* Back button */} К марафону {/* Header stats */}
{participation?.total_points || 0}
Очков
{participation?.current_streak || 0}
Серия
{participation?.drop_count || 0}
Пропусков
{/* Active event banner */} {activeEvent?.event && (
)} {/* Tabs for Common Enemy event */} {activeEvent?.event?.type === 'common_enemy' && (
)} {/* Event tab content (Common Enemy) */} {activeTab === 'event' && activeEvent?.event?.type === 'common_enemy' && ( <> {/* Common Enemy Leaderboard */}

Выполнили челлендж

{commonEnemyLeaderboard.length > 0 && ( {commonEnemyLeaderboard.length} чел. )}
{commonEnemyLeaderboard.length === 0 ? (
Пока никто не выполнил. Будь первым!
) : (
{commonEnemyLeaderboard.map((entry) => (
{entry.rank && entry.rank <= 3 ? ( ) : ( entry.rank )}

{entry.user.nickname}

{entry.bonus_points > 0 && ( +{entry.bonus_points} бонус )}
))}
)}
{/* Event Assignment Card */} {eventAssignment?.assignment && !eventAssignment.is_completed ? (
Задание события "Общий враг"
{/* Game */}

Игра

{eventAssignment.assignment.challenge.game.title}

{/* Challenge */}

Задание

{eventAssignment.assignment.challenge.title}

{eventAssignment.assignment.challenge.description}

{/* Points */}
+{eventAssignment.assignment.challenge.points} очков {eventAssignment.assignment.challenge.difficulty} {eventAssignment.assignment.challenge.estimated_time && ( ~{eventAssignment.assignment.challenge.estimated_time} мин )}
{/* Proof hint */} {eventAssignment.assignment.challenge.proof_hint && (

Нужно доказательство: {eventAssignment.assignment.challenge.proof_hint}

)} {/* Proof upload */}
{/* File upload */} setEventProofFile(e.target.files?.[0] || null)} /> {eventProofFile ? (
{eventProofFile.name}
) : ( )}
или
{/* URL input */} setEventProofUrl(e.target.value)} /> {/* Comment */}