rework shop
This commit is contained in:
@@ -5,7 +5,7 @@ import { useToast } from '@/store/toast'
|
||||
import { GlassCard, NeonButton, FramePreview } from '@/components/ui'
|
||||
import {
|
||||
Loader2, Package, ShoppingBag, Coins, Check,
|
||||
Frame, Type, Palette, Image, Zap, Shield, RefreshCw, SkipForward
|
||||
Frame, Type, Palette, Image, Zap, SkipForward, Shuffle, Dice5, Copy, Undo2
|
||||
} from 'lucide-react'
|
||||
import type { InventoryItem, ShopItemType } from '@/types'
|
||||
import { RARITY_COLORS, RARITY_NAMES, ITEM_TYPE_NAMES } from '@/types'
|
||||
@@ -13,9 +13,11 @@ import clsx from 'clsx'
|
||||
|
||||
const CONSUMABLE_ICONS: Record<string, React.ReactNode> = {
|
||||
skip: <SkipForward className="w-8 h-8" />,
|
||||
shield: <Shield className="w-8 h-8" />,
|
||||
boost: <Zap className="w-8 h-8" />,
|
||||
reroll: <RefreshCw className="w-8 h-8" />,
|
||||
wild_card: <Shuffle className="w-8 h-8" />,
|
||||
lucky_dice: <Dice5 className="w-8 h-8" />,
|
||||
copycat: <Copy className="w-8 h-8" />,
|
||||
undo: <Undo2 className="w-8 h-8" />,
|
||||
}
|
||||
|
||||
interface InventoryItemCardProps {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Marathon, Assignment, Game, ActiveEvent, SwapCandidate, MySwapRequ
|
||||
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, Shield, RefreshCw, SkipForward, Package } from 'lucide-react'
|
||||
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'
|
||||
@@ -494,45 +494,6 @@ export function PlayPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
@@ -541,7 +502,7 @@ export function PlayPage() {
|
||||
item_code: 'boost',
|
||||
marathon_id: parseInt(id),
|
||||
})
|
||||
toast.success('Boost активирован! x1.5 очков за следующее выполнение.')
|
||||
toast.success('Boost активирован! x1.5 очков за текущее задание.')
|
||||
await loadData()
|
||||
useShopStore.getState().loadBalance()
|
||||
} catch (err: unknown) {
|
||||
@@ -552,6 +513,119 @@ export function PlayPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<SwapCandidate[]>([])
|
||||
const [isLoadingCopycatCandidates, setIsLoadingCopycatCandidates] = useState(false)
|
||||
|
||||
const loadCopycatCandidates = async () => {
|
||||
if (!id) return
|
||||
setIsLoadingCopycatCandidates(true)
|
||||
try {
|
||||
const candidates = await eventsApi.getSwapCandidates(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 (
|
||||
<div className="flex flex-col items-center justify-center py-24">
|
||||
@@ -710,18 +784,18 @@ export function PlayPage() {
|
||||
</div>
|
||||
|
||||
{/* Active effects */}
|
||||
{(consumablesStatus.has_shield || consumablesStatus.has_active_boost) && (
|
||||
{(consumablesStatus.has_active_boost || consumablesStatus.has_lucky_dice) && (
|
||||
<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)
|
||||
<Zap className="w-3 h-3" /> Boost x1.5
|
||||
</span>
|
||||
)}
|
||||
{consumablesStatus.has_lucky_dice && (
|
||||
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 text-xs rounded-lg border border-purple-500/30 flex items-center gap-1">
|
||||
<Dice5 className="w-3 h-3" /> Lucky Dice x{consumablesStatus.lucky_dice_multiplier}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -752,52 +826,6 @@ export function PlayPage() {
|
||||
</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">
|
||||
@@ -821,10 +849,180 @@ export function PlayPage() {
|
||||
{consumablesStatus.has_active_boost ? 'Активен' : 'Активировать'}
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Wild Card */}
|
||||
<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">
|
||||
<Shuffle className="w-4 h-4 text-green-400" />
|
||||
<span className="text-white font-medium">Wild Card</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{consumablesStatus.wild_cards_available} шт.</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Выбрать игру</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setShowWildCardModal(true)}
|
||||
disabled={consumablesStatus.wild_cards_available === 0 || !currentAssignment || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'wild_card'}
|
||||
className="w-full"
|
||||
>
|
||||
Выбрать
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Lucky Dice */}
|
||||
<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">
|
||||
<Dice5 className="w-4 h-4 text-purple-400" />
|
||||
<span className="text-white font-medium">Lucky Dice</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">
|
||||
{consumablesStatus.has_lucky_dice ? `x${consumablesStatus.lucky_dice_multiplier}` : `${consumablesStatus.lucky_dice_available} шт.`}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Случайный множитель</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseLuckyDice}
|
||||
disabled={consumablesStatus.has_lucky_dice || consumablesStatus.lucky_dice_available === 0 || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'lucky_dice'}
|
||||
className="w-full"
|
||||
>
|
||||
{consumablesStatus.has_lucky_dice ? `x${consumablesStatus.lucky_dice_multiplier}` : 'Бросить'}
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Copycat */}
|
||||
<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">
|
||||
<Copy className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-white font-medium">Copycat</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{consumablesStatus.copycats_available} шт.</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Скопировать задание</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowCopycatModal(true)
|
||||
loadCopycatCandidates()
|
||||
}}
|
||||
disabled={consumablesStatus.copycats_available === 0 || !currentAssignment || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'copycat'}
|
||||
className="w-full"
|
||||
>
|
||||
Выбрать
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Undo */}
|
||||
<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">
|
||||
<Undo2 className="w-4 h-4 text-red-400" />
|
||||
<span className="text-white font-medium">Undo</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{consumablesStatus.undos_available} шт.</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Отменить дроп</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseUndo}
|
||||
disabled={!consumablesStatus.can_undo || consumablesStatus.undos_available === 0 || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'undo'}
|
||||
className="w-full"
|
||||
>
|
||||
{consumablesStatus.can_undo ? 'Отменить' : 'Нет дропа'}
|
||||
</NeonButton>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
)}
|
||||
|
||||
{/* Wild Card Modal */}
|
||||
{showWildCardModal && (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<GlassCard className="w-full max-w-md max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold text-white">Выберите игру</h3>
|
||||
<button
|
||||
onClick={() => setShowWildCardModal(false)}
|
||||
className="p-2 text-gray-400 hover:text-white"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mb-4">
|
||||
Вы получите случайное задание из выбранной игры
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{games.map((game) => (
|
||||
<button
|
||||
key={game.id}
|
||||
onClick={() => handleUseWildCard(game.id)}
|
||||
disabled={isUsingConsumable === 'wild_card'}
|
||||
className="w-full p-3 bg-dark-700/50 hover:bg-dark-700 rounded-xl text-left transition-colors border border-dark-600 hover:border-green-500/30 disabled:opacity-50"
|
||||
>
|
||||
<p className="text-white font-medium">{game.title}</p>
|
||||
<p className="text-gray-400 text-xs">{game.challenges_count} челленджей</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Copycat Modal */}
|
||||
{showCopycatModal && (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<GlassCard className="w-full max-w-md max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold text-white">Скопировать задание</h3>
|
||||
<button
|
||||
onClick={() => setShowCopycatModal(false)}
|
||||
className="p-2 text-gray-400 hover:text-white"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mb-4">
|
||||
Выберите участника, чьё задание хотите скопировать
|
||||
</p>
|
||||
{isLoadingCopycatCandidates ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-400" />
|
||||
</div>
|
||||
) : copycatCandidates.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-8">Нет доступных заданий для копирования</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{copycatCandidates.map((candidate) => (
|
||||
<button
|
||||
key={candidate.participant_id}
|
||||
onClick={() => handleUseCopycat(candidate.participant_id)}
|
||||
disabled={isUsingConsumable === 'copycat'}
|
||||
className="w-full p-3 bg-dark-700/50 hover:bg-dark-700 rounded-xl text-left transition-colors border border-dark-600 hover:border-cyan-500/30 disabled:opacity-50"
|
||||
>
|
||||
<p className="text-white font-medium">{candidate.user.nickname}</p>
|
||||
<p className="text-cyan-400 text-sm">{candidate.challenge_title}</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
{candidate.game_title} • {candidate.challenge_points} очков • {candidate.challenge_difficulty}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</GlassCard>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs for Common Enemy event */}
|
||||
{activeEvent?.event?.type === 'common_enemy' && (
|
||||
<div className="flex gap-2 mb-6">
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useConfirm } from '@/store/confirm'
|
||||
import { GlassCard, NeonButton, FramePreview } from '@/components/ui'
|
||||
import {
|
||||
Loader2, Coins, ShoppingBag, Package, Sparkles,
|
||||
Frame, Type, Palette, Image, Zap, Shield, RefreshCw, SkipForward,
|
||||
Minus, Plus
|
||||
Frame, Type, Palette, Image, Zap, SkipForward,
|
||||
Minus, Plus, Shuffle, Dice5, Copy, Undo2
|
||||
} from 'lucide-react'
|
||||
import type { ShopItem, ShopItemType, ShopItemPublic } from '@/types'
|
||||
import { RARITY_COLORS, RARITY_NAMES } from '@/types'
|
||||
@@ -23,9 +23,11 @@ const ITEM_TYPE_ICONS: Record<ShopItemType, React.ReactNode> = {
|
||||
|
||||
const CONSUMABLE_ICONS: Record<string, React.ReactNode> = {
|
||||
skip: <SkipForward className="w-8 h-8" />,
|
||||
shield: <Shield className="w-8 h-8" />,
|
||||
boost: <Zap className="w-8 h-8" />,
|
||||
reroll: <RefreshCw className="w-8 h-8" />,
|
||||
wild_card: <Shuffle className="w-8 h-8" />,
|
||||
lucky_dice: <Dice5 className="w-8 h-8" />,
|
||||
copycat: <Copy className="w-8 h-8" />,
|
||||
undo: <Undo2 className="w-8 h-8" />,
|
||||
}
|
||||
|
||||
interface ShopItemCardProps {
|
||||
@@ -176,7 +178,7 @@ function ShopItemCard({ item, onPurchase, isPurchasing }: ShopItemCardProps) {
|
||||
className={clsx(
|
||||
'p-4 border transition-all duration-300',
|
||||
rarityColors.border,
|
||||
item.is_owned && 'opacity-60'
|
||||
item.is_owned && !isConsumable && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
{/* Rarity badge */}
|
||||
@@ -196,7 +198,7 @@ function ShopItemCard({ item, onPurchase, isPurchasing }: ShopItemCardProps) {
|
||||
</p>
|
||||
|
||||
{/* Quantity selector for consumables */}
|
||||
{isConsumable && !item.is_owned && item.is_available && (
|
||||
{isConsumable && item.is_available && (
|
||||
<div className="flex items-center justify-center gap-2 mb-3">
|
||||
<button
|
||||
onClick={decrementQuantity}
|
||||
@@ -236,7 +238,7 @@ function ShopItemCard({ item, onPurchase, isPurchasing }: ShopItemCardProps) {
|
||||
) : (
|
||||
<NeonButton
|
||||
size="sm"
|
||||
onClick={() => onPurchase(item, quantity)}
|
||||
onClick={() => onPurchase(item, isConsumable ? quantity : 1)}
|
||||
disabled={isPurchasing || !item.is_available}
|
||||
>
|
||||
{isPurchasing ? (
|
||||
@@ -460,9 +462,9 @@ export function ShopPage() {
|
||||
</h3>
|
||||
<ul className="text-gray-400 text-sm space-y-1">
|
||||
<li>• Выполняй задания в <span className="text-neon-400">сертифицированных</span> марафонах</li>
|
||||
<li>• Easy задание — 5 монет, Medium — 12 монет, Hard — 25 монет</li>
|
||||
<li>• Playthrough — ~5% от заработанных очков</li>
|
||||
<li>• Топ-3 места в марафоне: 1-е — 100, 2-е — 50, 3-е — 30 монет</li>
|
||||
<li>• Easy задание — 10 монет, Medium — 20 монет, Hard — 35 монет</li>
|
||||
<li>• Playthrough — ~10% от заработанных очков</li>
|
||||
<li>• Топ-3 места в марафоне: 1-е — 500, 2-е — 250, 3-е — 150 монет</li>
|
||||
</ul>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
@@ -718,7 +718,7 @@ export interface PasswordChangeData {
|
||||
|
||||
export type ShopItemType = 'frame' | 'title' | 'name_color' | 'background' | 'consumable'
|
||||
export type ItemRarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
|
||||
export type ConsumableType = 'skip' | 'shield' | 'boost' | 'reroll'
|
||||
export type ConsumableType = 'skip' | 'boost' | 'wild_card' | 'lucky_dice' | 'copycat' | 'undo'
|
||||
|
||||
export interface ShopItemPublic {
|
||||
id: number
|
||||
@@ -776,6 +776,8 @@ export interface UseConsumableRequest {
|
||||
item_code: ConsumableType
|
||||
marathon_id: number
|
||||
assignment_id?: number
|
||||
game_id?: number // Required for wild_card
|
||||
target_participant_id?: number // Required for copycat
|
||||
}
|
||||
|
||||
export interface UseConsumableResponse {
|
||||
@@ -805,12 +807,16 @@ export interface ConsumablesStatus {
|
||||
skips_available: number
|
||||
skips_used: number
|
||||
skips_remaining: number | null
|
||||
shields_available: number
|
||||
has_shield: boolean
|
||||
boosts_available: number
|
||||
has_active_boost: boolean
|
||||
boost_multiplier: number | null
|
||||
rerolls_available: number
|
||||
wild_cards_available: number
|
||||
lucky_dice_available: number
|
||||
has_lucky_dice: boolean
|
||||
lucky_dice_multiplier: number | null
|
||||
copycats_available: number
|
||||
undos_available: number
|
||||
can_undo: boolean
|
||||
}
|
||||
|
||||
export interface UserCosmetics {
|
||||
|
||||
Reference in New Issue
Block a user