Add manual add for challanges
This commit is contained in:
@@ -32,6 +32,16 @@ const EVENT_ICONS: Record<EventType, React.ReactNode> = {
|
||||
game_choice: <Gamepad2 className="w-4 h-4" />,
|
||||
}
|
||||
|
||||
// Default durations for events (in minutes)
|
||||
const DEFAULT_DURATIONS: Record<EventType, number | null> = {
|
||||
golden_hour: 45,
|
||||
common_enemy: null, // Until all complete
|
||||
double_risk: 120,
|
||||
jackpot: null, // 1 spin
|
||||
swap: 60,
|
||||
game_choice: 120,
|
||||
}
|
||||
|
||||
export function EventControl({
|
||||
marathonId,
|
||||
activeEvent,
|
||||
@@ -42,9 +52,17 @@ export function EventControl({
|
||||
const confirm = useConfirm()
|
||||
const [selectedType, setSelectedType] = useState<EventType>('golden_hour')
|
||||
const [selectedChallengeId, setSelectedChallengeId] = useState<number | null>(null)
|
||||
const [durationMinutes, setDurationMinutes] = useState<number | ''>(45)
|
||||
const [isStarting, setIsStarting] = useState(false)
|
||||
const [isStopping, setIsStopping] = useState(false)
|
||||
|
||||
// Update duration when event type changes
|
||||
const handleTypeChange = (type: EventType) => {
|
||||
setSelectedType(type)
|
||||
const defaultDuration = DEFAULT_DURATIONS[type]
|
||||
setDurationMinutes(defaultDuration ?? '')
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
if (selectedType === 'common_enemy' && !selectedChallengeId) {
|
||||
toast.warning('Выберите челлендж для события "Общий враг"')
|
||||
@@ -55,6 +73,7 @@ export function EventControl({
|
||||
try {
|
||||
await eventsApi.start(marathonId, {
|
||||
type: selectedType,
|
||||
duration_minutes: durationMinutes || undefined,
|
||||
challenge_id: selectedType === 'common_enemy' ? selectedChallengeId ?? undefined : undefined,
|
||||
})
|
||||
onEventChange()
|
||||
@@ -119,7 +138,7 @@ export function EventControl({
|
||||
{EVENT_TYPES.map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setSelectedType(type)}
|
||||
onClick={() => handleTypeChange(type)}
|
||||
className={`
|
||||
p-3 rounded-lg border-2 transition-all text-left
|
||||
${selectedType === type
|
||||
@@ -138,6 +157,27 @@ export function EventControl({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Duration setting */}
|
||||
{DEFAULT_DURATIONS[selectedType] !== null && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Длительность (минуты)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={durationMinutes}
|
||||
onChange={(e) => setDurationMinutes(e.target.value ? parseInt(e.target.value) : '')}
|
||||
min={1}
|
||||
max={480}
|
||||
placeholder={`По умолчанию: ${DEFAULT_DURATIONS[selectedType]}`}
|
||||
className="w-full p-2 bg-gray-700 border border-gray-600 rounded-lg text-white"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Оставьте пустым для значения по умолчанию ({DEFAULT_DURATIONS[selectedType]} мин)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedType === 'common_enemy' && challenges && challenges.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
|
||||
@@ -45,6 +45,20 @@ export function LobbyPage() {
|
||||
const [gameChallenges, setGameChallenges] = useState<Record<number, Challenge[]>>({})
|
||||
const [loadingChallenges, setLoadingChallenges] = useState<number | null>(null)
|
||||
|
||||
// Manual challenge creation
|
||||
const [addingChallengeToGameId, setAddingChallengeToGameId] = useState<number | null>(null)
|
||||
const [newChallenge, setNewChallenge] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
type: 'completion',
|
||||
difficulty: 'medium',
|
||||
points: 50,
|
||||
estimated_time: 30,
|
||||
proof_type: 'screenshot',
|
||||
proof_hint: '',
|
||||
})
|
||||
const [isCreatingChallenge, setIsCreatingChallenge] = useState(false)
|
||||
|
||||
// Start marathon
|
||||
const [isStarting, setIsStarting] = useState(false)
|
||||
|
||||
@@ -161,6 +175,7 @@ export function LobbyPage() {
|
||||
|
||||
setExpandedGameId(gameId)
|
||||
|
||||
// Load challenges if we haven't loaded them yet
|
||||
if (!gameChallenges[gameId]) {
|
||||
setLoadingChallenges(gameId)
|
||||
try {
|
||||
@@ -168,12 +183,57 @@ export function LobbyPage() {
|
||||
setGameChallenges(prev => ({ ...prev, [gameId]: challenges }))
|
||||
} catch (error) {
|
||||
console.error('Failed to load challenges:', error)
|
||||
// Set empty array to prevent repeated attempts
|
||||
setGameChallenges(prev => ({ ...prev, [gameId]: [] }))
|
||||
} finally {
|
||||
setLoadingChallenges(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateChallenge = async (gameId: number) => {
|
||||
if (!newChallenge.title.trim() || !newChallenge.description.trim()) {
|
||||
toast.warning('Заполните название и описание')
|
||||
return
|
||||
}
|
||||
|
||||
setIsCreatingChallenge(true)
|
||||
try {
|
||||
await gamesApi.createChallenge(gameId, {
|
||||
title: newChallenge.title.trim(),
|
||||
description: newChallenge.description.trim(),
|
||||
type: newChallenge.type,
|
||||
difficulty: newChallenge.difficulty,
|
||||
points: newChallenge.points,
|
||||
estimated_time: newChallenge.estimated_time || undefined,
|
||||
proof_type: newChallenge.proof_type,
|
||||
proof_hint: newChallenge.proof_hint.trim() || undefined,
|
||||
})
|
||||
toast.success('Задание добавлено')
|
||||
// Reset form
|
||||
setNewChallenge({
|
||||
title: '',
|
||||
description: '',
|
||||
type: 'completion',
|
||||
difficulty: 'medium',
|
||||
points: 50,
|
||||
estimated_time: 30,
|
||||
proof_type: 'screenshot',
|
||||
proof_hint: '',
|
||||
})
|
||||
setAddingChallengeToGameId(null)
|
||||
// Refresh challenges
|
||||
const challenges = await gamesApi.getChallenges(gameId)
|
||||
setGameChallenges(prev => ({ ...prev, [gameId]: challenges }))
|
||||
await loadData()
|
||||
} catch (err: unknown) {
|
||||
const error = err as { response?: { data?: { detail?: string } } }
|
||||
toast.error(error.response?.data?.detail || 'Не удалось добавить задание')
|
||||
} finally {
|
||||
setIsCreatingChallenge(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteChallenge = async (challengeId: number, gameId: number) => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Удалить задание?',
|
||||
@@ -320,12 +380,12 @@ export function LobbyPage() {
|
||||
{/* Game header */}
|
||||
<div
|
||||
className={`flex items-center justify-between p-4 ${
|
||||
game.challenges_count > 0 ? 'cursor-pointer hover:bg-gray-800/50' : ''
|
||||
(game.status === 'approved') ? 'cursor-pointer hover:bg-gray-800/50' : ''
|
||||
}`}
|
||||
onClick={() => game.challenges_count > 0 && handleToggleGameChallenges(game.id)}
|
||||
onClick={() => game.status === 'approved' && handleToggleGameChallenges(game.id)}
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{game.challenges_count > 0 && (
|
||||
{game.status === 'approved' && (
|
||||
<span className="text-gray-400 shrink-0">
|
||||
{expandedGameId === game.id ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
@@ -398,7 +458,9 @@ export function LobbyPage() {
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : gameChallenges[game.id]?.length > 0 ? (
|
||||
) : (
|
||||
<>
|
||||
{gameChallenges[game.id]?.length > 0 ? (
|
||||
gameChallenges[game.id].map((challenge) => (
|
||||
<div
|
||||
key={challenge.id}
|
||||
@@ -443,6 +505,132 @@ export function LobbyPage() {
|
||||
Нет заданий
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Add challenge form */}
|
||||
{isOrganizer && game.status === 'approved' && (
|
||||
addingChallengeToGameId === game.id ? (
|
||||
<div className="mt-4 p-4 bg-gray-800 rounded-lg space-y-3 border border-gray-700">
|
||||
<h4 className="font-medium text-white text-sm">Новое задание</h4>
|
||||
<Input
|
||||
placeholder="Название задания"
|
||||
value={newChallenge.title}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, title: e.target.value }))}
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Описание (что нужно сделать)"
|
||||
value={newChallenge.description}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, description: e.target.value }))}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm resize-none"
|
||||
rows={2}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 mb-1 block">Тип</label>
|
||||
<select
|
||||
value={newChallenge.type}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, type: e.target.value }))}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="completion">Прохождение</option>
|
||||
<option value="no_death">Без смертей</option>
|
||||
<option value="speedrun">Спидран</option>
|
||||
<option value="collection">Коллекция</option>
|
||||
<option value="achievement">Достижение</option>
|
||||
<option value="challenge_run">Челлендж-ран</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 mb-1 block">Сложность</label>
|
||||
<select
|
||||
value={newChallenge.difficulty}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, difficulty: e.target.value }))}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="easy">Легко (20-40 очков)</option>
|
||||
<option value="medium">Средне (45-75 очков)</option>
|
||||
<option value="hard">Сложно (90-150 очков)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 mb-1 block">Очки</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newChallenge.points}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, points: parseInt(e.target.value) || 0 }))}
|
||||
min={1}
|
||||
max={500}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 mb-1 block">Время (мин)</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={newChallenge.estimated_time}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, estimated_time: parseInt(e.target.value) || 0 }))}
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 mb-1 block">Тип доказательства</label>
|
||||
<select
|
||||
value={newChallenge.proof_type}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, proof_type: e.target.value }))}
|
||||
className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white text-sm"
|
||||
>
|
||||
<option value="screenshot">Скриншот</option>
|
||||
<option value="video">Видео</option>
|
||||
<option value="steam">Steam</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 mb-1 block">Подсказка</label>
|
||||
<Input
|
||||
placeholder="Что должно быть на пруфе"
|
||||
value={newChallenge.proof_hint}
|
||||
onChange={(e) => setNewChallenge(prev => ({ ...prev, proof_hint: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleCreateChallenge(game.id)}
|
||||
isLoading={isCreatingChallenge}
|
||||
disabled={!newChallenge.title || !newChallenge.description}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Добавить
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setAddingChallengeToGameId(null)}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setAddingChallengeToGameId(game.id)
|
||||
setExpandedGameId(game.id)
|
||||
}}
|
||||
className="w-full mt-2 border border-dashed border-gray-700 text-gray-400 hover:text-white hover:border-gray-600"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Добавить задание вручную
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user