Zaebalsya

This commit is contained in:
2025-12-17 20:19:26 +07:00
parent 7e7cdbcd76
commit 0b3837b08e
6 changed files with 267 additions and 35 deletions

View File

@@ -39,6 +39,8 @@ export function LobbyPage() {
const [previewChallenges, setPreviewChallenges] = useState<ChallengePreview[] | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [editingIndex, setEditingIndex] = useState<number | null>(null)
const [showGenerateSelection, setShowGenerateSelection] = useState(false)
const [selectedGamesForGeneration, setSelectedGamesForGeneration] = useState<number[]>([])
// View existing challenges
const [expandedGameId, setExpandedGameId] = useState<number | null>(null)
@@ -253,11 +255,14 @@ export function LobbyPage() {
setIsGenerating(true)
setGenerateMessage(null)
try {
const result = await gamesApi.previewChallenges(parseInt(id))
// Pass selected games if any, otherwise generate for all games without challenges
const gameIds = selectedGamesForGeneration.length > 0 ? selectedGamesForGeneration : undefined
const result = await gamesApi.previewChallenges(parseInt(id), gameIds)
if (result.challenges.length === 0) {
setGenerateMessage('Все игры уже имеют задания')
setGenerateMessage('Нет игр для генерации заданий')
} else {
setPreviewChallenges(result.challenges)
setShowGenerateSelection(false)
}
} catch (error) {
console.error('Failed to generate challenges:', error)
@@ -267,6 +272,22 @@ export function LobbyPage() {
}
}
const toggleGameSelection = (gameId: number) => {
setSelectedGamesForGeneration(prev =>
prev.includes(gameId)
? prev.filter(id => id !== gameId)
: [...prev, gameId]
)
}
const selectAllGamesForGeneration = () => {
setSelectedGamesForGeneration(approvedGames.map(g => g.id))
}
const clearGameSelection = () => {
setSelectedGamesForGeneration([])
}
const handleSaveChallenges = async () => {
if (!id || !previewChallenges) return
@@ -703,7 +724,7 @@ export function LobbyPage() {
{/* Generate challenges */}
{isOrganizer && approvedGames.length > 0 && !previewChallenges && (
<GlassCard className="mb-8">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="flex items-center justify-between gap-4 flex-wrap mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-500/20 flex items-center justify-center">
<Zap className="w-5 h-5 text-accent-400" />
@@ -711,20 +732,100 @@ export function LobbyPage() {
<div>
<h3 className="font-semibold text-white">Генерация заданий</h3>
<p className="text-sm text-gray-400">
ИИ создаст задания для игр без заданий
{showGenerateSelection
? `Выбрано: ${selectedGamesForGeneration.length} из ${approvedGames.length}`
: 'Выберите игры для генерации'}
</p>
</div>
</div>
<NeonButton
onClick={handleGenerateChallenges}
isLoading={isGenerating}
variant="outline"
color="purple"
icon={<Sparkles className="w-4 h-4" />}
>
Сгенерировать
</NeonButton>
<div className="flex gap-2">
{showGenerateSelection ? (
<>
<NeonButton
onClick={() => {
setShowGenerateSelection(false)
clearGameSelection()
}}
variant="secondary"
size="sm"
>
Отмена
</NeonButton>
<NeonButton
onClick={handleGenerateChallenges}
isLoading={isGenerating}
color="purple"
size="sm"
icon={<Sparkles className="w-4 h-4" />}
disabled={selectedGamesForGeneration.length === 0}
>
Сгенерировать ({selectedGamesForGeneration.length})
</NeonButton>
</>
) : (
<NeonButton
onClick={() => setShowGenerateSelection(true)}
variant="outline"
color="purple"
icon={<Sparkles className="w-4 h-4" />}
>
Выбрать игры
</NeonButton>
)}
</div>
</div>
{/* Game selection */}
{showGenerateSelection && (
<div className="space-y-3">
<div className="flex items-center justify-between text-sm">
<button
onClick={selectAllGamesForGeneration}
className="text-neon-400 hover:text-neon-300 transition-colors"
>
Выбрать все
</button>
<button
onClick={clearGameSelection}
className="text-gray-400 hover:text-gray-300 transition-colors"
>
Снять выбор
</button>
</div>
<div className="grid gap-2">
{approvedGames.map((game) => {
const isSelected = selectedGamesForGeneration.includes(game.id)
const challengeCount = gameChallenges[game.id]?.length ?? game.challenges_count
return (
<button
key={game.id}
onClick={() => toggleGameSelection(game.id)}
className={`flex items-center gap-3 p-3 rounded-lg border transition-all text-left ${
isSelected
? 'bg-accent-500/20 border-accent-500/50'
: 'bg-dark-700/50 border-dark-600 hover:border-dark-500'
}`}
>
<div className={`w-5 h-5 rounded flex items-center justify-center border-2 transition-colors ${
isSelected
? 'bg-accent-500 border-accent-500'
: 'border-gray-500'
}`}>
{isSelected && <Check className="w-3 h-3 text-white" />}
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-medium truncate">{game.title}</p>
<p className="text-xs text-gray-400">
{challengeCount > 0 ? `${challengeCount} заданий` : 'Нет заданий'}
</p>
</div>
</button>
)
})}
</div>
</div>
)}
{generateMessage && (
<p className="mt-4 text-sm text-neon-400 p-3 bg-neon-500/10 rounded-lg border border-neon-500/20">
{generateMessage}

View File

@@ -12,7 +12,7 @@ import { ActivityFeed, type ActivityFeedRef } from '@/components/ActivityFeed'
import {
Users, Calendar, Trophy, Play, Settings, Copy, Check, Loader2, Trash2,
Globe, Lock, CalendarCheck, UserPlus, Gamepad2, ArrowLeft, Zap, Flag,
Star, Target, TrendingDown, ChevronDown, ChevronUp, Link2
Star, Target, TrendingDown, ChevronDown, ChevronUp, Link2, Sparkles
} from 'lucide-react'
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
@@ -32,6 +32,8 @@ export function MarathonPage() {
const [isJoining, setIsJoining] = useState(false)
const [isFinishing, setIsFinishing] = useState(false)
const [showEventControl, setShowEventControl] = useState(false)
const [showChallenges, setShowChallenges] = useState(false)
const [expandedGameId, setExpandedGameId] = useState<number | null>(null)
const activityFeedRef = useRef<ActivityFeedRef>(null)
useEffect(() => {
@@ -48,13 +50,12 @@ export function MarathonPage() {
const eventData = await eventsApi.getActive(parseInt(id))
setActiveEvent(eventData)
if (data.my_participation.role === 'organizer') {
try {
const challengesData = await challengesApi.list(parseInt(id))
setChallenges(challengesData)
} catch {
// Ignore if no challenges
}
// Load challenges for all participants
try {
const challengesData = await challengesApi.list(parseInt(id))
setChallenges(challengesData)
} catch {
// Ignore if no challenges
}
}
} catch (error) {
@@ -411,6 +412,108 @@ export function MarathonPage() {
</div>
</GlassCard>
)}
{/* All challenges viewer */}
{marathon.status === 'active' && isParticipant && challenges.length > 0 && (
<GlassCard>
<button
onClick={() => setShowChallenges(!showChallenges)}
className="w-full flex items-center justify-between"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-accent-500/20 flex items-center justify-center">
<Sparkles className="w-5 h-5 text-accent-400" />
</div>
<div className="text-left">
<h3 className="font-semibold text-white">Все задания</h3>
<p className="text-sm text-gray-400">{challenges.length} заданий для {new Set(challenges.map(c => c.game.id)).size} игр</p>
</div>
</div>
{showChallenges ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</button>
{showChallenges && (
<div className="mt-6 pt-6 border-t border-dark-600 space-y-4">
{/* Group challenges by game */}
{Array.from(new Set(challenges.map(c => c.game.id))).map(gameId => {
const gameChallenges = challenges.filter(c => c.game.id === gameId)
const game = gameChallenges[0]?.game
if (!game) return null
const isExpanded = expandedGameId === gameId
return (
<div key={gameId} className="glass rounded-xl overflow-hidden border border-dark-600">
<button
onClick={() => setExpandedGameId(isExpanded ? null : gameId)}
className="w-full flex items-center justify-between p-4 hover:bg-dark-700/50 transition-colors"
>
<div className="flex items-center gap-3">
<span className="text-gray-400">
{isExpanded ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</span>
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-neon-500/20 to-accent-500/20 border border-neon-500/20 flex items-center justify-center">
<Gamepad2 className="w-4 h-4 text-neon-400" />
</div>
<div className="text-left">
<h4 className="font-semibold text-white">{game.title}</h4>
<span className="text-xs text-gray-400">{gameChallenges.length} заданий</span>
</div>
</div>
</button>
{isExpanded && (
<div className="border-t border-dark-600 p-4 space-y-2 bg-dark-800/30">
{gameChallenges.map(challenge => (
<div
key={challenge.id}
className="p-3 bg-dark-700/50 rounded-lg border border-dark-600"
>
<div className="flex items-center gap-2 mb-1 flex-wrap">
<span className={`text-xs px-2 py-0.5 rounded-lg border ${
challenge.difficulty === 'easy' ? 'bg-green-500/20 text-green-400 border-green-500/30' :
challenge.difficulty === 'medium' ? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30' :
'bg-red-500/20 text-red-400 border-red-500/30'
}`}>
{challenge.difficulty === 'easy' ? 'Легко' :
challenge.difficulty === 'medium' ? 'Средне' : 'Сложно'}
</span>
<span className="text-xs text-neon-400 font-semibold">
+{challenge.points}
</span>
<span className="text-xs text-gray-500">
{challenge.type === 'completion' ? 'Прохождение' :
challenge.type === 'no_death' ? 'Без смертей' :
challenge.type === 'speedrun' ? 'Спидран' :
challenge.type === 'collection' ? 'Коллекция' :
challenge.type === 'achievement' ? 'Достижение' : 'Челлендж-ран'}
</span>
</div>
<h5 className="text-sm font-medium text-white">{challenge.title}</h5>
<p className="text-xs text-gray-400 mt-1">{challenge.description}</p>
{challenge.proof_hint && (
<p className="text-xs text-gray-500 mt-2 flex items-center gap-1">
<Target className="w-3 h-3" />
Пруф: {challenge.proof_hint}
</p>
)}
</div>
))}
</div>
)}
</div>
)
})}
</div>
)}
</GlassCard>
)}
</div>
{/* Activity Feed - right sidebar */}