import { useState, useEffect, useRef } from 'react' import { useParams, Link } from 'react-router-dom' import { marathonsApi, wheelApi, gamesApi, eventsApi, assignmentsApi, shopApi } from '@/api' import type { Marathon, Assignment, Game, ActiveEvent, SwapCandidate, MySwapRequests, CommonEnemyLeaderboardEntry, EventAssignment, GameChoiceChallenges, ReturnedAssignment, ConsumablesStatus, ConsumableType } from '@/types' import { NeonButton, GlassCard, StatsCard } 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, Zap, Flame, Target, Download, SkipForward, Package, Dice5, Copy, Undo2, Shuffle } from 'lucide-react' import { useToast } from '@/store/toast' import { useConfirm } from '@/store/confirm' import { useShopStore } from '@/store/shop' const MAX_IMAGE_SIZE = 15 * 1024 * 1024 const MAX_VIDEO_SIZE = 30 * 1024 * 1024 const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'] const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov'] 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 [games, setGames] = useState([]) const [activeEvent, setActiveEvent] = useState(null) const [isLoading, setIsLoading] = useState(true) const [proofFiles, setProofFiles] = useState([]) const [proofUrl, setProofUrl] = useState('') const [comment, setComment] = useState('') const [isCompleting, setIsCompleting] = useState(false) const [isDropping, setIsDropping] = useState(false) const [selectedGameId, setSelectedGameId] = useState(null) const [gameChoiceChallenges, setGameChoiceChallenges] = useState(null) const [isLoadingChallenges, setIsLoadingChallenges] = useState(false) const [isSelectingChallenge, setIsSelectingChallenge] = useState(false) 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) const [commonEnemyLeaderboard, setCommonEnemyLeaderboard] = useState([]) type PlayTab = 'spin' | 'event' const [activeTab, setActiveTab] = useState('spin') const [eventAssignment, setEventAssignment] = useState(null) const [eventProofFile, setEventProofFile] = useState(null) const [eventProofUrl, setEventProofUrl] = useState('') const [eventComment, setEventComment] = useState('') const [isEventCompleting, setIsEventCompleting] = useState(false) const [returnedAssignments, setReturnedAssignments] = useState([]) // Consumables const [consumablesStatus, setConsumablesStatus] = useState(null) const [isUsingConsumable, setIsUsingConsumable] = useState(null) // Bonus challenge completion const [expandedBonusId, setExpandedBonusId] = useState(null) const [bonusProofFiles, setBonusProofFiles] = useState([]) const [bonusProofUrl, setBonusProofUrl] = useState('') const [bonusComment, setBonusComment] = useState('') const [isCompletingBonus, setIsCompletingBonus] = useState(false) const bonusFileInputRef = useRef(null) const fileInputRef = useRef(null) const eventFileInputRef = useRef(null) useEffect(() => { loadData() }, [id]) useEffect(() => { if (activeEvent?.event?.type !== 'game_choice') { setSelectedGameId(null) setGameChoiceChallenges(null) } }, [activeEvent?.event?.type]) useEffect(() => { if (activeEvent?.event?.type === 'swap') { loadSwapRequests() if (currentAssignment) { loadSwapCandidates() } } }, [activeEvent?.event?.type, currentAssignment]) useEffect(() => { if (activeEvent?.event?.type === 'common_enemy') { loadCommonEnemyLeaderboard() 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 validateAndSetFile = ( file: File | null, setFile: (file: File | null) => void, inputRef: React.RefObject ) => { if (!file) { setFile(null) return } const ext = file.name.split('.').pop()?.toLowerCase() || '' const isImage = IMAGE_EXTENSIONS.includes(ext) const isVideo = VIDEO_EXTENSIONS.includes(ext) if (!isImage && !isVideo) { toast.error('Неподдерживаемый формат файла') if (inputRef.current) inputRef.current.value = '' return } const maxSize = isImage ? MAX_IMAGE_SIZE : MAX_VIDEO_SIZE const maxSizeMB = isImage ? 15 : 30 if (file.size > maxSize) { toast.error(`Файл слишком большой. Максимум ${maxSizeMB} МБ для ${isImage ? 'изображений' : 'видео'}`) if (inputRef.current) inputRef.current.value = '' return } setFile(file) } const loadData = async () => { if (!id) return try { const [marathonData, assignment, availableGamesData, eventData, eventAssignmentData, returnedData, consumablesData] = await Promise.all([ marathonsApi.get(parseInt(id)), wheelApi.getCurrentAssignment(parseInt(id)), gamesApi.getAvailableGames(parseInt(id)), eventsApi.getActive(parseInt(id)), eventsApi.getEventAssignment(parseInt(id)), assignmentsApi.getReturnedAssignments(parseInt(id)), shopApi.getConsumablesStatus(parseInt(id)).catch(() => null), ]) setMarathon(marathonData) setCurrentAssignment(assignment) setGames(availableGamesData) setActiveEvent(eventData) setEventAssignment(eventAssignmentData) setReturnedAssignments(returnedData) setConsumablesStatus(consumablesData) } 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)) 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 () => { setTimeout(async () => { await loadData() }, 500) } const handleComplete = async () => { if (!currentAssignment) return // For playthrough: allow file, URL, or comment // For challenges: require file or URL if (currentAssignment.is_playthrough) { if (proofFiles.length === 0 && !proofUrl && !comment) { toast.warning('Пожалуйста, предоставьте доказательство (файл, ссылку или комментарий)') return } } else { if (proofFiles.length === 0 && !proofUrl) { toast.warning('Пожалуйста, предоставьте доказательство (файл или ссылку)') return } } setIsCompleting(true) try { const result = await wheelApi.complete(currentAssignment.id, { proof_files: proofFiles.length > 0 ? proofFiles : undefined, proof_url: proofUrl || undefined, comment: comment || undefined, }) toast.success(`Выполнено! +${result.points_earned} очков (бонус серии: +${result.streak_bonus})`) setProofFiles([]) setProofUrl('') setComment('') await loadData() // Refresh coins balance useShopStore.getState().loadBalance() } 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 = currentAssignment.drop_penalty 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} очков`) await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось пропустить') } finally { setIsDropping(false) } } const handleBonusComplete = async (bonusId: number) => { if (!currentAssignment) return if (bonusProofFiles.length === 0 && !bonusProofUrl && !bonusComment) { toast.warning('Прикрепите файл, ссылку или комментарий') return } setIsCompletingBonus(true) try { const result = await assignmentsApi.completeBonusAssignment( currentAssignment.id, bonusId, { proof_files: bonusProofFiles.length > 0 ? bonusProofFiles : undefined, proof_url: bonusProofUrl || undefined, comment: bonusComment || undefined, } ) toast.success(`Бонус отмечен! +${result.points_earned} очков начислится при завершении игры`) setBonusProofFiles([]) setBonusProofUrl('') setBonusComment('') setExpandedBonusId(null) if (bonusFileInputRef.current) bonusFileInputRef.current.value = '' await loadData() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось выполнить бонус') } finally { setIsCompletingBonus(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} очков`) 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) } } // Consumable handlers const handleUseSkip = async () => { if (!currentAssignment || !id) return setIsUsingConsumable('skip') try { await shopApi.useConsumable({ item_code: 'skip', marathon_id: parseInt(id), assignment_id: currentAssignment.id, }) toast.success('Задание пропущено без штрафа!') await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось использовать Skip') } finally { setIsUsingConsumable(null) } } const handleUseSkipExile = async () => { if (!currentAssignment || !id) return const confirmed = await confirm({ title: 'Скип с изгнанием?', message: 'Задание будет пропущено без штрафа, а игра навсегда удалена из вашего пула.', confirmText: 'Использовать', cancelText: 'Отмена', variant: 'warning', }) if (!confirmed) return setIsUsingConsumable('skip_exile') try { await shopApi.useConsumable({ item_code: 'skip_exile', marathon_id: parseInt(id), assignment_id: currentAssignment.id, }) toast.success('Задание пропущено, игра изгнана из пула!') await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось использовать Skip с изгнанием') } finally { setIsUsingConsumable(null) } } const handleUseBoost = async () => { if (!id) return setIsUsingConsumable('boost') try { await shopApi.useConsumable({ item_code: 'boost', marathon_id: parseInt(id), }) toast.success('Boost активирован! x1.5 очков за текущее задание.') await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось активировать Boost') } finally { setIsUsingConsumable(null) } } // Wild Card modal state const [showWildCardModal, setShowWildCardModal] = useState(false) const handleUseWildCard = async (gameId: number) => { if (!currentAssignment || !id) return setIsUsingConsumable('wild_card') try { const result = await shopApi.useConsumable({ item_code: 'wild_card', marathon_id: parseInt(id), assignment_id: currentAssignment.id, game_id: gameId, }) toast.success(result.effect_description) setShowWildCardModal(false) await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось использовать Wild Card') } finally { setIsUsingConsumable(null) } } const handleUseLuckyDice = async () => { if (!id) return setIsUsingConsumable('lucky_dice') try { const result = await shopApi.useConsumable({ item_code: 'lucky_dice', marathon_id: parseInt(id), }) const multiplier = result.effect_data?.multiplier as number toast.success(`Lucky Dice: x${multiplier} множитель!`) await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось использовать Lucky Dice') } finally { setIsUsingConsumable(null) } } // Copycat modal state const [showCopycatModal, setShowCopycatModal] = useState(false) const [copycatCandidates, setCopycatCandidates] = useState([]) const [isLoadingCopycatCandidates, setIsLoadingCopycatCandidates] = useState(false) const loadCopycatCandidates = async () => { if (!id) return setIsLoadingCopycatCandidates(true) try { const candidates = await shopApi.getCopycatCandidates(parseInt(id)) setCopycatCandidates(candidates) } catch (error) { console.error('Failed to load copycat candidates:', error) } finally { setIsLoadingCopycatCandidates(false) } } const handleUseCopycat = async (targetParticipantId: number) => { if (!currentAssignment || !id) return setIsUsingConsumable('copycat') try { const result = await shopApi.useConsumable({ item_code: 'copycat', marathon_id: parseInt(id), assignment_id: currentAssignment.id, target_participant_id: targetParticipantId, }) toast.success(result.effect_description) setShowCopycatModal(false) await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось использовать Copycat') } finally { setIsUsingConsumable(null) } } const handleUseUndo = async () => { if (!id) return const confirmed = await confirm({ title: 'Использовать Undo?', message: 'Это вернёт очки и серию от последнего пропуска.', confirmText: 'Использовать', cancelText: 'Отмена', variant: 'info', }) if (!confirmed) return setIsUsingConsumable('undo') try { const result = await shopApi.useConsumable({ item_code: 'undo', marathon_id: parseInt(id), }) toast.success(result.effect_description) await loadData() useShopStore.getState().loadBalance() } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } toast.error(error.response?.data?.detail || 'Не удалось использовать Undo') } finally { setIsUsingConsumable(null) } } if (isLoading) { return (

Загрузка...

) } if (!marathon) { return (

Марафон не найден

) } const marathonEndDate = marathon.end_date ? new Date(marathon.end_date) : null const isMarathonExpired = marathonEndDate && new Date() > marathonEndDate const isMarathonEnded = marathon.status === 'finished' || isMarathonExpired if (isMarathonEnded) { return (
К марафону

Марафон завершён

{marathon.status === 'finished' ? 'Этот марафон был завершён организатором.' : 'Этот марафон завершился по истечении срока.'}

}> Посмотреть итоговый рейтинг
) } const participation = marathon.my_participation return (
{/* Back button */} К марафону {/* Header stats */}
} color="neon" /> } color="purple" /> } color="default" />
{/* Active event banner */} {activeEvent?.event && (
)} {/* Returned assignments warning */} {returnedAssignments.length > 0 && (

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

{returnedAssignments.length} заданий

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

{returnedAssignments.map((ra) => (
{ra.is_playthrough ? ( <>

Прохождение: {ra.game_title}

Прохождение игры

) : ra.challenge ? ( <>

{ra.challenge.title}

{ra.challenge.game.title}

) : null}
{!ra.is_playthrough && ra.challenge && ( +{ra.challenge.points} )}

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

))}
)} {/* Consumables Panel */} {consumablesStatus && marathon?.allow_consumables && (

Расходники

Используйте для облегчения задания

{/* Active effects */} {(consumablesStatus.has_active_boost || consumablesStatus.has_lucky_dice) && (

Активные эффекты:

{consumablesStatus.has_active_boost && ( Boost x1.5 )} {consumablesStatus.has_lucky_dice && ( Lucky Dice x{consumablesStatus.lucky_dice_multiplier} )}
)} {/* Consumables grid */}
{/* Skip */}
Skip
{consumablesStatus.skips_available} шт.

Пропустить без штрафа

Использовать
{/* Skip with Exile */}
Skip + Изгнание
{consumablesStatus.skip_exiles_available} шт.

Скип + убрать игру из пула

Использовать
{/* Boost */}
Boost
{consumablesStatus.has_active_boost ? 'Активен' : `${consumablesStatus.boosts_available} шт.`}

x1.5 очков

{consumablesStatus.has_active_boost ? 'Активен' : 'Активировать'}
{/* Wild Card */}
Wild Card
{consumablesStatus.wild_cards_available} шт.

Выбрать игру

setShowWildCardModal(true)} disabled={consumablesStatus.wild_cards_available === 0 || !currentAssignment || isUsingConsumable !== null} isLoading={isUsingConsumable === 'wild_card'} className="w-full" > Выбрать
{/* Lucky Dice */}
Lucky Dice
{consumablesStatus.has_lucky_dice ? `x${consumablesStatus.lucky_dice_multiplier}` : `${consumablesStatus.lucky_dice_available} шт.`}

Случайный множитель

{consumablesStatus.has_lucky_dice ? `x${consumablesStatus.lucky_dice_multiplier}` : 'Бросить'}
{/* Copycat */}
Copycat
{consumablesStatus.copycats_available} шт.

Скопировать задание

{ setShowCopycatModal(true) loadCopycatCandidates() }} disabled={consumablesStatus.copycats_available === 0 || !currentAssignment || isUsingConsumable !== null} isLoading={isUsingConsumable === 'copycat'} className="w-full" > Выбрать
{/* Undo */}
Undo
{consumablesStatus.undos_available} шт.

Отменить дроп

{consumablesStatus.can_undo ? 'Отменить' : 'Нет дропа'}
)} {/* Wild Card Modal */} {showWildCardModal && (

Выберите игру

Вы получите случайное задание из выбранной игры

{games.map((game) => ( ))}
)} {/* Copycat Modal */} {showCopycatModal && (

Скопировать задание

Выберите участника, чьё задание хотите скопировать

{isLoadingCopycatCandidates ? (
) : copycatCandidates.length === 0 ? (

Нет доступных заданий для копирования

) : (
{copycatCandidates.map((candidate) => { const displayTitle = candidate.is_playthrough ? `Прохождение: ${candidate.game_title}` : candidate.challenge_title || '' const displayDetails = candidate.is_playthrough ? `${candidate.playthrough_points || 0} очков` : `${candidate.game_title} • ${candidate.challenge_points} очков • ${candidate.challenge_difficulty}` return ( ) })}
)}
)} {/* Tabs for Common Enemy event */} {activeEvent?.event?.type === 'common_enemy' && (
setActiveTab('spin')} className="flex-1" > Мой прокрут setActiveTab('event')} className="flex-1 relative" color="purple" > Общий враг {eventAssignment?.assignment && !eventAssignment.is_completed && ( )}
)} {/* Event tab content (Common Enemy) */} {activeTab === 'event' && activeEvent?.event?.type === 'common_enemy' && ( <> {/* Common Enemy Leaderboard */}

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

{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 ? (
Задание события "Общий враг"

Игра

{eventAssignment.assignment.challenge?.game.title}

Задание

{eventAssignment.assignment.challenge?.title}

{eventAssignment.assignment.challenge?.description}

+{eventAssignment.assignment.challenge?.points} очков {eventAssignment.assignment.challenge?.difficulty} {eventAssignment.assignment.challenge?.estimated_time && ( ~{eventAssignment.assignment.challenge.estimated_time} мин )}
{eventAssignment.assignment.challenge?.proof_hint && (

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

)}
validateAndSetFile(e.target.files?.[0] || null, setEventProofFile, eventFileInputRef)} /> {eventProofFile ? (
{eventProofFile.name}
) : (
eventFileInputRef.current?.click()} icon={} > Выбрать файл

Макс. 15 МБ для изображений, 30 МБ для видео

)}
или
setEventProofUrl(e.target.value)} />