Bug fixes
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
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, Game, ActiveEvent, SwapCandidate, MySwapRequests, CommonEnemyLeaderboardEntry, EventAssignment, GameChoiceChallenges, ReturnedAssignment } from '@/types'
|
||||
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 } from 'lucide-react'
|
||||
import { Loader2, Upload, X, Gamepad2, ArrowLeftRight, Check, XCircle, Clock, Send, Trophy, Users, ArrowLeft, AlertTriangle, Zap, Flame, Target, Download, Shield, RefreshCw, SkipForward, Package } 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
|
||||
@@ -55,6 +56,10 @@ export function PlayPage() {
|
||||
|
||||
const [returnedAssignments, setReturnedAssignments] = useState<ReturnedAssignment[]>([])
|
||||
|
||||
// Consumables
|
||||
const [consumablesStatus, setConsumablesStatus] = useState<ConsumablesStatus | null>(null)
|
||||
const [isUsingConsumable, setIsUsingConsumable] = useState<ConsumableType | null>(null)
|
||||
|
||||
// Bonus challenge completion
|
||||
const [expandedBonusId, setExpandedBonusId] = useState<number | null>(null)
|
||||
const [bonusProofFiles, setBonusProofFiles] = useState<File[]>([])
|
||||
@@ -177,13 +182,14 @@ export function PlayPage() {
|
||||
const loadData = async () => {
|
||||
if (!id) return
|
||||
try {
|
||||
const [marathonData, assignment, availableGamesData, eventData, eventAssignmentData, returnedData] = await Promise.all([
|
||||
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)
|
||||
@@ -191,6 +197,7 @@ export function PlayPage() {
|
||||
setActiveEvent(eventData)
|
||||
setEventAssignment(eventAssignmentData)
|
||||
setReturnedAssignments(returnedData)
|
||||
setConsumablesStatus(consumablesData)
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error)
|
||||
} finally {
|
||||
@@ -255,6 +262,8 @@ export function PlayPage() {
|
||||
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 || 'Не удалось выполнить')
|
||||
@@ -464,6 +473,85 @@ export function PlayPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 handleUseReroll = async () => {
|
||||
if (!currentAssignment || !id) return
|
||||
setIsUsingConsumable('reroll')
|
||||
try {
|
||||
await shopApi.useConsumable({
|
||||
item_code: 'reroll',
|
||||
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 || 'Не удалось использовать Reroll')
|
||||
} finally {
|
||||
setIsUsingConsumable(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUseShield = async () => {
|
||||
if (!id) return
|
||||
setIsUsingConsumable('shield')
|
||||
try {
|
||||
await shopApi.useConsumable({
|
||||
item_code: 'shield',
|
||||
marathon_id: parseInt(id),
|
||||
})
|
||||
toast.success('Shield активирован! Следующий пропуск будет бесплатным.')
|
||||
await loadData()
|
||||
useShopStore.getState().loadBalance()
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { detail?: string } } }
|
||||
toast.error(error.response?.data?.detail || 'Не удалось активировать Shield')
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-24">
|
||||
@@ -608,6 +696,135 @@ export function PlayPage() {
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
{/* Consumables Panel */}
|
||||
{consumablesStatus && marathon?.allow_consumables && (
|
||||
<GlassCard className="mb-6 border-purple-500/30">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple-500/20 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-purple-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-purple-400">Расходники</h3>
|
||||
<p className="text-sm text-gray-400">Используйте для облегчения задания</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active effects */}
|
||||
{(consumablesStatus.has_shield || consumablesStatus.has_active_boost) && (
|
||||
<div className="mb-4 p-3 bg-green-500/10 border border-green-500/30 rounded-xl">
|
||||
<p className="text-green-400 text-sm font-medium mb-2">Активные эффекты:</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{consumablesStatus.has_shield && (
|
||||
<span className="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-lg border border-blue-500/30 flex items-center gap-1">
|
||||
<Shield className="w-3 h-3" /> Shield (следующий drop бесплатный)
|
||||
</span>
|
||||
)}
|
||||
{consumablesStatus.has_active_boost && (
|
||||
<span className="px-2 py-1 bg-yellow-500/20 text-yellow-400 text-xs rounded-lg border border-yellow-500/30 flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" /> Boost x1.5 (следующий complete)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Consumables grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Skip */}
|
||||
<div className="p-3 bg-dark-700/50 rounded-xl border border-dark-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<SkipForward className="w-4 h-4 text-orange-400" />
|
||||
<span className="text-white font-medium">Skip</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{consumablesStatus.skips_available} шт.</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Пропустить без штрафа</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseSkip}
|
||||
disabled={consumablesStatus.skips_available === 0 || !currentAssignment || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'skip'}
|
||||
className="w-full"
|
||||
>
|
||||
Использовать
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Reroll */}
|
||||
<div className="p-3 bg-dark-700/50 rounded-xl border border-dark-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshCw className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-white font-medium">Reroll</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{consumablesStatus.rerolls_available} шт.</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Переспинить задание</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseReroll}
|
||||
disabled={consumablesStatus.rerolls_available === 0 || !currentAssignment || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'reroll'}
|
||||
className="w-full"
|
||||
>
|
||||
Использовать
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Shield */}
|
||||
<div className="p-3 bg-dark-700/50 rounded-xl border border-dark-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-white font-medium">Shield</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">
|
||||
{consumablesStatus.has_shield ? 'Активен' : `${consumablesStatus.shields_available} шт.`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Защита от штрафа</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseShield}
|
||||
disabled={consumablesStatus.has_shield || consumablesStatus.shields_available === 0 || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'shield'}
|
||||
className="w-full"
|
||||
>
|
||||
{consumablesStatus.has_shield ? 'Активен' : 'Активировать'}
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Boost */}
|
||||
<div className="p-3 bg-dark-700/50 rounded-xl border border-dark-600">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-yellow-400" />
|
||||
<span className="text-white font-medium">Boost</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">
|
||||
{consumablesStatus.has_active_boost ? 'Активен' : `${consumablesStatus.boosts_available} шт.`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">x1.5 очков</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseBoost}
|
||||
disabled={consumablesStatus.has_active_boost || consumablesStatus.boosts_available === 0 || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'boost'}
|
||||
className="w-full"
|
||||
>
|
||||
{consumablesStatus.has_active_boost ? 'Активен' : 'Активировать'}
|
||||
</NeonButton>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
{/* Tabs for Common Enemy event */}
|
||||
{activeEvent?.event?.type === 'common_enemy' && (
|
||||
<div className="flex gap-2 mb-6">
|
||||
|
||||
Reference in New Issue
Block a user