Change rematch event to change game
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
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 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, RotateCcw, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft } from 'lucide-react'
|
||||
import { Loader2, Upload, X, Gamepad2, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft } from 'lucide-react'
|
||||
|
||||
export function PlayPage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
@@ -26,10 +26,11 @@ export function PlayPage() {
|
||||
// Drop state
|
||||
const [isDropping, setIsDropping] = useState(false)
|
||||
|
||||
// Rematch state
|
||||
const [droppedAssignments, setDroppedAssignments] = useState<DroppedAssignment[]>([])
|
||||
const [isRematchLoading, setIsRematchLoading] = useState(false)
|
||||
const [rematchingId, setRematchingId] = useState<number | null>(null)
|
||||
// Game Choice state
|
||||
const [selectedGameId, setSelectedGameId] = useState<number | null>(null)
|
||||
const [gameChoiceChallenges, setGameChoiceChallenges] = useState<GameChoiceChallenges | null>(null)
|
||||
const [isLoadingChallenges, setIsLoadingChallenges] = useState(false)
|
||||
const [isSelectingChallenge, setIsSelectingChallenge] = useState(false)
|
||||
|
||||
// Swap state
|
||||
const [swapCandidates, setSwapCandidates] = useState<SwapCandidate[]>([])
|
||||
@@ -59,12 +60,13 @@ export function PlayPage() {
|
||||
loadData()
|
||||
}, [id])
|
||||
|
||||
// Load dropped assignments when rematch event is active
|
||||
// Reset game choice state when event changes or ends
|
||||
useEffect(() => {
|
||||
if (activeEvent?.event?.type === 'rematch' && !currentAssignment) {
|
||||
loadDroppedAssignments()
|
||||
if (activeEvent?.event?.type !== 'game_choice') {
|
||||
setSelectedGameId(null)
|
||||
setGameChoiceChallenges(null)
|
||||
}
|
||||
}, [activeEvent?.event?.type, currentAssignment])
|
||||
}, [activeEvent?.event?.type])
|
||||
|
||||
// Load swap candidates and requests when swap event is active
|
||||
useEffect(() => {
|
||||
@@ -86,16 +88,17 @@ export function PlayPage() {
|
||||
}
|
||||
}, [activeEvent?.event?.type])
|
||||
|
||||
const loadDroppedAssignments = async () => {
|
||||
const loadGameChoiceChallenges = async (gameId: number) => {
|
||||
if (!id) return
|
||||
setIsRematchLoading(true)
|
||||
setIsLoadingChallenges(true)
|
||||
try {
|
||||
const dropped = await eventsApi.getDroppedAssignments(parseInt(id))
|
||||
setDroppedAssignments(dropped)
|
||||
const challenges = await eventsApi.getGameChoiceChallenges(parseInt(id), gameId)
|
||||
setGameChoiceChallenges(challenges)
|
||||
} catch (error) {
|
||||
console.error('Failed to load dropped assignments:', error)
|
||||
console.error('Failed to load game choice challenges:', error)
|
||||
alert('Не удалось загрузить челленджи для этой игры')
|
||||
} finally {
|
||||
setIsRematchLoading(false)
|
||||
setIsLoadingChallenges(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,21 +272,33 @@ export function PlayPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRematch = async (assignmentId: number) => {
|
||||
const handleGameSelect = async (gameId: number) => {
|
||||
setSelectedGameId(gameId)
|
||||
await loadGameChoiceChallenges(gameId)
|
||||
}
|
||||
|
||||
const handleChallengeSelect = async (challengeId: number) => {
|
||||
if (!id) return
|
||||
|
||||
if (!confirm('Начать реванш? Вы получите 50% от обычных очков за выполнение.')) return
|
||||
const hasActiveAssignment = !!currentAssignment
|
||||
const confirmMessage = hasActiveAssignment
|
||||
? 'Выбрать этот челлендж? Текущее задание будет заменено без штрафа.'
|
||||
: 'Выбрать этот челлендж?'
|
||||
|
||||
setRematchingId(assignmentId)
|
||||
if (!confirm(confirmMessage)) return
|
||||
|
||||
setIsSelectingChallenge(true)
|
||||
try {
|
||||
await eventsApi.rematch(parseInt(id), assignmentId)
|
||||
alert('Реванш начат! Выполните задание за 50% очков.')
|
||||
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 || 'Не удалось начать реванш')
|
||||
alert(error.response?.data?.detail || 'Не удалось выбрать челлендж')
|
||||
} finally {
|
||||
setRematchingId(null)
|
||||
setIsSelectingChallenge(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,122 +669,56 @@ export function PlayPage() {
|
||||
<>
|
||||
{/* Common Enemy Leaderboard - show on spin tab too for context */}
|
||||
{activeEvent?.event?.type === 'common_enemy' && activeTab === 'spin' && commonEnemyLeaderboard.length > 0 && (
|
||||
<Card className="mb-6">
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Users className="w-5 h-5 text-red-500" />
|
||||
<h3 className="text-lg font-bold text-white">Выполнили челлендж</h3>
|
||||
{commonEnemyLeaderboard.length > 0 && (
|
||||
<span className="ml-auto text-gray-400 text-sm">
|
||||
{commonEnemyLeaderboard.length} чел.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{commonEnemyLeaderboard.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500">
|
||||
Пока никто не выполнил. Будь первым!
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{commonEnemyLeaderboard.map((entry) => (
|
||||
<div
|
||||
key={entry.participant_id}
|
||||
className={`
|
||||
flex items-center gap-3 p-3 rounded-lg
|
||||
${entry.rank === 1 ? 'bg-yellow-500/20 border border-yellow-500/30' :
|
||||
entry.rank === 2 ? 'bg-gray-400/20 border border-gray-400/30' :
|
||||
entry.rank === 3 ? 'bg-orange-600/20 border border-orange-600/30' :
|
||||
'bg-gray-800'}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm
|
||||
${entry.rank === 1 ? 'bg-yellow-500 text-black' :
|
||||
entry.rank === 2 ? 'bg-gray-400 text-black' :
|
||||
entry.rank === 3 ? 'bg-orange-600 text-white' :
|
||||
'bg-gray-700 text-gray-300'}
|
||||
`}>
|
||||
{entry.rank && entry.rank <= 3 ? (
|
||||
<Trophy className="w-4 h-4" />
|
||||
) : (
|
||||
entry.rank
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium">{entry.user.nickname}</p>
|
||||
</div>
|
||||
{entry.bonus_points > 0 && (
|
||||
<span className="text-green-400 text-sm font-medium">
|
||||
+{entry.bonus_points} бонус
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* No active assignment - show spin wheel */}
|
||||
{!currentAssignment && (
|
||||
<>
|
||||
<Card>
|
||||
<CardContent className="py-8">
|
||||
<h2 className="text-2xl font-bold text-white mb-2 text-center">Крутите колесо!</h2>
|
||||
<p className="text-gray-400 mb-6 text-center">
|
||||
Получите случайную игру и задание для выполнения
|
||||
</p>
|
||||
<SpinWheel
|
||||
games={games}
|
||||
onSpin={handleSpin}
|
||||
onSpinComplete={handleSpinComplete}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Rematch section - show during rematch event */}
|
||||
{activeEvent?.event?.type === 'rematch' && droppedAssignments.length > 0 && (
|
||||
<Card className="mt-6">
|
||||
<Card className="mb-6">
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<RotateCcw className="w-5 h-5 text-orange-500" />
|
||||
<h3 className="text-lg font-bold text-white">Реванш</h3>
|
||||
<Users className="w-5 h-5 text-red-500" />
|
||||
<h3 className="text-lg font-bold text-white">Выполнили челлендж</h3>
|
||||
{commonEnemyLeaderboard.length > 0 && (
|
||||
<span className="ml-auto text-gray-400 text-sm">
|
||||
{commonEnemyLeaderboard.length} чел.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mb-4">
|
||||
Во время события "Реванш" вы можете повторить пропущенные задания за 50% очков
|
||||
</p>
|
||||
|
||||
{isRematchLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-gray-500" />
|
||||
{commonEnemyLeaderboard.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500">
|
||||
Пока никто не выполнил. Будь первым!
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{droppedAssignments.map((dropped) => (
|
||||
<div className="space-y-2">
|
||||
{commonEnemyLeaderboard.map((entry) => (
|
||||
<div
|
||||
key={dropped.id}
|
||||
className="flex items-center justify-between p-3 bg-gray-900 rounded-lg"
|
||||
key={entry.participant_id}
|
||||
className={`
|
||||
flex items-center gap-3 p-3 rounded-lg
|
||||
${entry.rank === 1 ? 'bg-yellow-500/20 border border-yellow-500/30' :
|
||||
entry.rank === 2 ? 'bg-gray-400/20 border border-gray-400/30' :
|
||||
entry.rank === 3 ? 'bg-orange-600/20 border border-orange-600/30' :
|
||||
'bg-gray-800'}
|
||||
`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium truncate">
|
||||
{dropped.challenge.title}
|
||||
</p>
|
||||
<p className="text-gray-400 text-sm">
|
||||
{dropped.challenge.game.title} • {dropped.challenge.points * 0.5} очков
|
||||
</p>
|
||||
<div className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm
|
||||
${entry.rank === 1 ? 'bg-yellow-500 text-black' :
|
||||
entry.rank === 2 ? 'bg-gray-400 text-black' :
|
||||
entry.rank === 3 ? 'bg-orange-600 text-white' :
|
||||
'bg-gray-700 text-gray-300'}
|
||||
`}>
|
||||
{entry.rank && entry.rank <= 3 ? (
|
||||
<Trophy className="w-4 h-4" />
|
||||
) : (
|
||||
entry.rank
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => handleRematch(dropped.id)}
|
||||
isLoading={rematchingId === dropped.id}
|
||||
disabled={rematchingId !== null}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4 mr-1" />
|
||||
Реванш
|
||||
</Button>
|
||||
<div className="flex-1">
|
||||
<p className="text-white font-medium">{entry.user.nickname}</p>
|
||||
</div>
|
||||
{entry.bonus_points > 0 && (
|
||||
<span className="text-green-400 text-sm font-medium">
|
||||
+{entry.bonus_points} бонус
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -777,8 +726,121 @@ export function PlayPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Game Choice section - show ABOVE spin wheel during game_choice event (works with or without assignment) */}
|
||||
{activeEvent?.event?.type === 'game_choice' && (
|
||||
<Card className="mb-6">
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Gamepad2 className="w-5 h-5 text-orange-500" />
|
||||
<h3 className="text-lg font-bold text-white">Выбор игры</h3>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mb-4">
|
||||
Выберите игру и один из 3 челленджей. {currentAssignment ? 'Текущее задание будет заменено без штрафа!' : ''}
|
||||
</p>
|
||||
|
||||
{/* Game selection */}
|
||||
{!selectedGameId && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{games.map((game) => (
|
||||
<button
|
||||
key={game.id}
|
||||
onClick={() => handleGameSelect(game.id)}
|
||||
className="p-3 bg-gray-900 hover:bg-gray-800 rounded-lg text-left transition-colors"
|
||||
>
|
||||
<p className="text-white font-medium truncate">{game.title}</p>
|
||||
<p className="text-gray-400 text-xs">{game.challenges_count} челленджей</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Challenge selection */}
|
||||
{selectedGameId && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-white font-medium">
|
||||
{gameChoiceChallenges?.game_title || 'Загрузка...'}
|
||||
</h4>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedGameId(null)
|
||||
setGameChoiceChallenges(null)
|
||||
}}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-1" />
|
||||
Назад
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoadingChallenges ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-gray-500" />
|
||||
</div>
|
||||
) : gameChoiceChallenges?.challenges.length ? (
|
||||
<div className="space-y-3">
|
||||
{gameChoiceChallenges.challenges.map((challenge) => (
|
||||
<div
|
||||
key={challenge.id}
|
||||
className="p-4 bg-gray-900 rounded-lg"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium">{challenge.title}</p>
|
||||
<p className="text-gray-400 text-sm mt-1">{challenge.description}</p>
|
||||
<div className="flex items-center gap-2 mt-2 text-xs">
|
||||
<span className="px-2 py-0.5 bg-green-500/20 text-green-400 rounded">
|
||||
+{challenge.points} очков
|
||||
</span>
|
||||
<span className="px-2 py-0.5 bg-gray-700 text-gray-300 rounded">
|
||||
{challenge.difficulty}
|
||||
</span>
|
||||
{challenge.estimated_time && (
|
||||
<span className="text-gray-500">~{challenge.estimated_time} мин</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleChallengeSelect(challenge.id)}
|
||||
isLoading={isSelectingChallenge}
|
||||
disabled={isSelectingChallenge}
|
||||
>
|
||||
Выбрать
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-center text-gray-500 py-4">
|
||||
Нет доступных челленджей для этой игры
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* No active assignment - show spin wheel */}
|
||||
{!currentAssignment && (
|
||||
<Card>
|
||||
<CardContent className="py-8">
|
||||
<h2 className="text-2xl font-bold text-white mb-2 text-center">Крутите колесо!</h2>
|
||||
<p className="text-gray-400 mb-6 text-center">
|
||||
Получите случайную игру и задание для выполнения
|
||||
</p>
|
||||
<SpinWheel
|
||||
games={games}
|
||||
onSpin={handleSpin}
|
||||
onSpinComplete={handleSpinComplete}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Active assignment */}
|
||||
{currentAssignment && (
|
||||
|
||||
Reference in New Issue
Block a user