import { useState, useEffect, useRef } from 'react' import { useParams, Link } from 'react-router-dom' import { marathonsApi, wheelApi, gamesApi, eventsApi, assignmentsApi } from '@/api' import type { Marathon, Assignment, SpinResult, Game, ActiveEvent, SwapCandidate, MySwapRequests, CommonEnemyLeaderboardEntry, EventAssignment, GameChoiceChallenges, ReturnedAssignment } 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, AlertTriangle } from 'lucide-react' import { useToast } from '@/store/toast' import { useConfirm } from '@/store/confirm' export function PlayPage() { const { id } = useParams<{ id: string }>() const toast = useToast() const confirm = useConfirm() 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) // Returned assignments state const [returnedAssignments, setReturnedAssignments] = useState([]) 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) toast.error('Не удалось загрузить челленджи для этой игры') } 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, returnedData] = await Promise.all([ marathonsApi.get(parseInt(id)), wheelApi.getCurrentAssignment(parseInt(id)), gamesApi.list(parseInt(id), 'approved'), eventsApi.getActive(parseInt(id)), eventsApi.getEventAssignment(parseInt(id)), assignmentsApi.getReturnedAssignments(parseInt(id)), ]) setMarathon(marathonData) setCurrentAssignment(assignment) setGames(gamesData) setActiveEvent(eventData) setEventAssignment(eventAssignmentData) setReturnedAssignments(returnedData) } 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 } } } toast.error(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) { toast.warning('Пожалуйста, предоставьте доказательство (файл или ссылку)') return } setIsCompleting(true) try { const result = await wheelApi.complete(currentAssignment.id, { proof_file: proofFile || undefined, proof_url: proofUrl || undefined, comment: comment || undefined, }) toast.success(`Выполнено! +${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 } } } toast.error(error.response?.data?.detail || 'Не удалось выполнить') } finally { setIsCompleting(false) } } const handleDrop = async () => { if (!currentAssignment) return const penalty = spinResult?.drop_penalty || 0 const confirmed = await confirm({ title: 'Пропустить задание?', message: `Вы потеряете ${penalty} очков.`, confirmText: 'Пропустить', cancelText: 'Отмена', variant: 'warning', }) if (!confirmed) return setIsDropping(true) try { const result = await wheelApi.drop(currentAssignment.id) toast.info(`Пропущено. Штраф: -${result.penalty} очков`) setSpinResult(null) await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось пропустить') } finally { setIsDropping(false) } } const handleEventComplete = async () => { if (!eventAssignment?.assignment) return if (!eventProofFile && !eventProofUrl) { toast.warning('Пожалуйста, предоставьте доказательство (файл или ссылку)') return } setIsEventCompleting(true) try { const result = await eventsApi.completeEventAssignment(eventAssignment.assignment.id, { proof_file: eventProofFile || undefined, proof_url: eventProofUrl || undefined, comment: eventComment || undefined, }) toast.success(`Выполнено! +${result.points_earned} очков`) // Reset form setEventProofFile(null) setEventProofUrl('') setEventComment('') await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(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 confirmed = await confirm({ title: 'Выбрать челлендж?', message: hasActiveAssignment ? 'Текущее задание будет заменено без штрафа.' : 'Вы уверены, что хотите выбрать этот челлендж?', confirmText: 'Выбрать', cancelText: 'Отмена', variant: 'info', }) if (!confirmed) return setIsSelectingChallenge(true) try { const result = await eventsApi.selectGameChoiceChallenge(parseInt(id), challengeId) toast.success(result.message) setSelectedGameId(null) setGameChoiceChallenges(null) await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось выбрать челлендж') } finally { setIsSelectingChallenge(false) } } const handleSendSwapRequest = async (participantId: number, participantName: string, theirChallenge: string) => { if (!id) return const confirmed = await confirm({ title: 'Отправить запрос на обмен?', message: `Вы предлагаете обменяться с ${participantName} на:\n"${theirChallenge}"\n\n${participantName} должен будет подтвердить обмен.`, confirmText: 'Отправить', cancelText: 'Отмена', variant: 'info', }) if (!confirmed) return setSendingRequestTo(participantId) try { await eventsApi.createSwapRequest(parseInt(id), participantId) toast.success('Запрос на обмен отправлен! Ожидайте подтверждения.') await loadSwapRequests() await loadSwapCandidates() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось отправить запрос') } finally { setSendingRequestTo(null) } } const handleAcceptSwapRequest = async (requestId: number) => { if (!id) return const confirmed = await confirm({ title: 'Принять обмен?', message: 'Задания будут обменяны сразу после подтверждения.', confirmText: 'Принять', cancelText: 'Отмена', variant: 'info', }) if (!confirmed) return setProcessingRequestId(requestId) try { await eventsApi.acceptSwapRequest(parseInt(id), requestId) toast.success('Обмен выполнен!') await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(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 } } } toast.error(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 } } } toast.error(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 && (
)} {/* Returned assignments warning */} {returnedAssignments.length > 0 && (

Возвращённые задания

{returnedAssignments.length}

Эти задания были оспорены. После текущего задания вам нужно будет их переделать.

{returnedAssignments.map((ra) => (

{ra.challenge.title}

{ra.challenge.game.title}

+{ra.challenge.points}

Причина: {ra.dispute_reason}

))}
)} {/* 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 */}