Добавлен Skip with Exile, модерация марафонов и выдача предметов
## Skip with Exile (новый расходник) - Новая модель ExiledGame для хранения изгнанных игр - Расходник skip_exile: пропуск без штрафа + игра исключается из пула навсегда - Фильтрация изгнанных игр при выдаче заданий - UI кнопка в PlayPage для использования skip_exile ## Модерация марафонов (для организаторов) - Эндпоинты: skip-assignment, exiled-games, restore-exiled-game - UI в LeaderboardPage: кнопка скипа у каждого участника - Выбор типа скипа (обычный/с изгнанием) + причина - Telegram уведомления о модерации ## Админская выдача предметов - Эндпоинты: admin grant/remove items, get user inventory - Новая страница AdminGrantItemPage (как магазин) - Telegram уведомление при получении подарка ## Исправления миграций - Миграции 029/030 теперь идемпотентны (проверка существования таблиц) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -494,6 +494,35 @@ export function PlayPage() {
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
@@ -826,6 +855,28 @@ export function PlayPage() {
|
||||
</NeonButton>
|
||||
</div>
|
||||
|
||||
{/* Skip with Exile */}
|
||||
<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">
|
||||
<XCircle className="w-4 h-4 text-red-400" />
|
||||
<span className="text-white font-medium">Skip + Изгнание</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{consumablesStatus.skip_exiles_available} шт.</span>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mb-2">Скип + убрать игру из пула</p>
|
||||
<NeonButton
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleUseSkipExile}
|
||||
disabled={consumablesStatus.skip_exiles_available === 0 || !currentAssignment || isUsingConsumable !== null}
|
||||
isLoading={isUsingConsumable === 'skip_exile'}
|
||||
className="w-full"
|
||||
>
|
||||
Использовать
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user