Zaebalsya
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user