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, SwapCandidate, MySwapRequests, CommonEnemyLeaderboardEntry, EventAssignment, GameChoiceChallenges } from '@/types' import { Button, Card, CardContent } from '@/components/ui' import { SpinWheel } from '@/components/SpinWheel' import { EventBanner } from '@/components/EventBanner' import { Loader2, Upload, X, Gamepad2, 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) // Game Choice state const [selectedGameId, setSelectedGameId] = useState(null) const [gameChoiceChallenges, setGameChoiceChallenges] = useState(null) const [isLoadingChallenges, setIsLoadingChallenges] = useState(false) const [isSelectingChallenge, setIsSelectingChallenge] = useState(false) // 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]) // Reset game choice state when event changes or ends useEffect(() => { if (activeEvent?.event?.type !== 'game_choice') { setSelectedGameId(null) setGameChoiceChallenges(null) } }, [activeEvent?.event?.type]) // 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 loadGameChoiceChallenges = async (gameId: number) => { if (!id) return setIsLoadingChallenges(true) try { const challenges = await eventsApi.getGameChoiceChallenges(parseInt(id), gameId) setGameChoiceChallenges(challenges) } catch (error) { console.error('Failed to load game choice challenges:', error) alert('Не удалось загрузить челленджи для этой игры') } finally { setIsLoadingChallenges(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 handleGameSelect = async (gameId: number) => { setSelectedGameId(gameId) await loadGameChoiceChallenges(gameId) } const handleChallengeSelect = async (challengeId: number) => { if (!id) return const hasActiveAssignment = !!currentAssignment const confirmMessage = hasActiveAssignment ? 'Выбрать этот челлендж? Текущее задание будет заменено без штрафа.' : 'Выбрать этот челлендж?' if (!confirm(confirmMessage)) return setIsSelectingChallenge(true) try { const result = await eventsApi.selectGameChoiceChallenge(parseInt(id), challengeId) alert(result.message) setSelectedGameId(null) setGameChoiceChallenges(null) await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } alert(error.response?.data?.detail || 'Не удалось выбрать челлендж') } finally { setIsSelectingChallenge(false) } } 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 */}