This commit is contained in:
2025-12-21 04:13:20 +07:00
parent 6bc35fc0bb
commit a513dc2207

View File

@@ -125,6 +125,14 @@ export function LobbyPage() {
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [generateSearchQuery, setGenerateSearchQuery] = useState('') const [generateSearchQuery, setGenerateSearchQuery] = useState('')
// Games list filters
const [filterProposer, setFilterProposer] = useState<number | 'all'>('all')
const [filterChallenges, setFilterChallenges] = useState<'all' | 'with' | 'without'>('all')
// Generation filters
const [generateFilterProposer, setGenerateFilterProposer] = useState<number | 'all'>('all')
const [generateFilterChallenges, setGenerateFilterChallenges] = useState<'all' | 'with' | 'without'>('all')
// Settings modal // Settings modal
const [showSettings, setShowSettings] = useState(false) const [showSettings, setShowSettings] = useState(false)
@@ -563,10 +571,6 @@ export function LobbyPage() {
) )
} }
const selectAllGamesForGeneration = () => {
setSelectedGamesForGeneration(approvedGames.map(g => g.id))
}
const clearGameSelection = () => { const clearGameSelection = () => {
setSelectedGamesForGeneration([]) setSelectedGamesForGeneration([])
} }
@@ -644,6 +648,22 @@ export function LobbyPage() {
const approvedGames = games.filter(g => g.status === 'approved') const approvedGames = games.filter(g => g.status === 'approved')
const totalChallenges = approvedGames.reduce((sum, g) => sum + g.challenges_count, 0) const totalChallenges = approvedGames.reduce((sum, g) => sum + g.challenges_count, 0)
// Get unique proposers for generation filter (from approved games)
const uniqueProposers = approvedGames.reduce((acc, game) => {
if (game.proposed_by && !acc.some(u => u.id === game.proposed_by?.id)) {
acc.push(game.proposed_by)
}
return acc
}, [] as { id: number; nickname: string }[])
// Get unique proposers for games list filter (from all games)
const allGamesProposers = games.reduce((acc, game) => {
if (game.proposed_by && !acc.some(u => u.id === game.proposed_by?.id)) {
acc.push(game.proposed_by)
}
return acc
}, [] as { id: number; nickname: string }[])
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
switch (status) { switch (status) {
case 'approved': case 'approved':
@@ -1422,6 +1442,8 @@ export function LobbyPage() {
setShowGenerateSelection(false) setShowGenerateSelection(false)
clearGameSelection() clearGameSelection()
setGenerateSearchQuery('') setGenerateSearchQuery('')
setGenerateFilterProposer('all')
setGenerateFilterChallenges('all')
}} }}
variant="secondary" variant="secondary"
size="sm" size="sm"
@@ -1455,7 +1477,7 @@ export function LobbyPage() {
{/* Game selection */} {/* Game selection */}
{showGenerateSelection && ( {showGenerateSelection && (
<div className="space-y-3"> <div className="space-y-3">
{/* Search in generation */} {/* Search */}
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input <input
@@ -1474,63 +1496,116 @@ export function LobbyPage() {
</button> </button>
)} )}
</div> </div>
<div className="flex items-center justify-between text-sm"> {/* Filters */}
<button <div className="flex gap-2">
onClick={selectAllGamesForGeneration} <select
className="text-neon-400 hover:text-neon-300 transition-colors" value={generateFilterProposer === 'all' ? 'all' : generateFilterProposer}
onChange={(e) => setGenerateFilterProposer(e.target.value === 'all' ? 'all' : parseInt(e.target.value))}
className="input py-2 text-sm flex-1"
> >
Выбрать все <option value="all">Все участники</option>
</button> {uniqueProposers.map(u => (
<button <option key={u.id} value={u.id}>{u.nickname}</option>
onClick={clearGameSelection} ))}
className="text-gray-400 hover:text-gray-300 transition-colors" </select>
<select
value={generateFilterChallenges}
onChange={(e) => setGenerateFilterChallenges(e.target.value as 'all' | 'with' | 'without')}
className="input py-2 text-sm flex-1"
> >
Снять выбор <option value="all">Все игры</option>
</button> <option value="with">С заданиями</option>
<option value="without">Без заданий</option>
</select>
</div> </div>
<div className="grid gap-2 max-h-64 overflow-y-auto custom-scrollbar"> {(() => {
{(() => { // Compute filtered games
const filteredGames = generateSearchQuery let filteredGames = approvedGames
? fuzzyFilter(approvedGames, generateSearchQuery, (g) => g.title + ' ' + (g.genre || ''))
: approvedGames
return filteredGames.length === 0 ? ( // Apply proposer filter
<p className="text-center text-gray-500 py-4 text-sm"> if (generateFilterProposer !== 'all') {
Ничего не найдено по запросу "{generateSearchQuery}" filteredGames = filteredGames.filter(g => g.proposed_by?.id === generateFilterProposer)
</p> }
) : (
filteredGames.map((game) => { // Apply challenges filter
const isSelected = selectedGamesForGeneration.includes(game.id) if (generateFilterChallenges === 'with') {
const challengeCount = gameChallenges[game.id]?.length ?? game.challenges_count filteredGames = filteredGames.filter(g => {
return ( const count = gameChallenges[g.id]?.length ?? g.challenges_count
<button return count > 0
key={game.id} })
onClick={() => toggleGameSelection(game.id)} } else if (generateFilterChallenges === 'without') {
className={`flex items-center gap-3 p-3 rounded-lg border transition-all text-left ${ filteredGames = filteredGames.filter(g => {
isSelected const count = gameChallenges[g.id]?.length ?? g.challenges_count
? 'bg-accent-500/20 border-accent-500/50' return count === 0
: '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 ${ // Apply search filter
isSelected if (generateSearchQuery) {
? 'bg-accent-500 border-accent-500' filteredGames = fuzzyFilter(filteredGames, generateSearchQuery, (g) => g.title + ' ' + (g.genre || ''))
: 'border-gray-500' }
}`}>
{isSelected && <Check className="w-3 h-3 text-white" />} return (
</div> <>
<div className="flex-1 min-w-0"> <div className="flex items-center justify-between text-sm">
<p className="text-white font-medium truncate">{game.title}</p> <button
<p className="text-xs text-gray-400"> onClick={() => setSelectedGamesForGeneration(filteredGames.map(g => g.id))}
{challengeCount > 0 ? `${challengeCount} заданий` : 'Нет заданий'} className="text-neon-400 hover:text-neon-300 transition-colors"
</p> >
</div> Выбрать все ({filteredGames.length})
</button> </button>
) <button
}) onClick={clearGameSelection}
) className="text-gray-400 hover:text-gray-300 transition-colors"
})()} >
</div> Снять выбор
</button>
</div>
<div className="grid gap-2 max-h-64 overflow-y-auto custom-scrollbar">
{filteredGames.length === 0 ? (
<p className="text-center text-gray-500 py-4 text-sm">
Ничего не найдено
</p>
) : (
filteredGames.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">
<div className="flex items-center gap-2">
<p className="text-white font-medium truncate">{game.title}</p>
{game.proposed_by && (
<span className="text-xs text-gray-500 shrink-0">от {game.proposed_by.nickname}</span>
)}
</div>
<p className="text-xs text-gray-400">
{challengeCount > 0 ? `${challengeCount} заданий` : 'Нет заданий'}
</p>
</div>
</button>
)
})
)}
</div>
</>
)
})()}
</div> </div>
)} )}
@@ -1702,24 +1777,47 @@ export function LobbyPage() {
)} )}
</div> </div>
{/* Search */} {/* Search and filters */}
<div className="relative mb-6"> <div className="space-y-3 mb-6">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" /> <div className="relative">
<input <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
type="text" <input
placeholder="Поиск игры..." type="text"
value={searchQuery} placeholder="Поиск игры..."
onChange={(e) => setSearchQuery(e.target.value)} value={searchQuery}
className="input w-full pl-10 pr-10" onChange={(e) => setSearchQuery(e.target.value)}
/> className="input w-full pl-10 pr-10"
{searchQuery && ( />
<button {searchQuery && (
onClick={() => setSearchQuery('')} <button
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors" onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
>
<X className="w-4 h-4" />
</button>
)}
</div>
<div className="flex gap-2">
<select
value={filterProposer === 'all' ? 'all' : filterProposer}
onChange={(e) => setFilterProposer(e.target.value === 'all' ? 'all' : parseInt(e.target.value))}
className="input py-2 text-sm flex-1"
> >
<X className="w-4 h-4" /> <option value="all">Все участники</option>
</button> {allGamesProposers.map(u => (
)} <option key={u.id} value={u.id}>{u.nickname}</option>
))}
</select>
<select
value={filterChallenges}
onChange={(e) => setFilterChallenges(e.target.value as 'all' | 'with' | 'without')}
className="input py-2 text-sm flex-1"
>
<option value="all">Все игры</option>
<option value="with">С заданиями</option>
<option value="without">Без заданий</option>
</select>
</div>
</div> </div>
{/* Add game form */} {/* Add game form */}
@@ -1763,26 +1861,47 @@ export function LobbyPage() {
{/* Games */} {/* Games */}
{(() => { {(() => {
const baseGames = isOrganizer let filteredGames = isOrganizer
? games.filter(g => g.status !== 'pending') ? games.filter(g => g.status !== 'pending')
: games.filter(g => g.status === 'approved' || (g.status === 'pending' && g.proposed_by?.id === user?.id)) : games.filter(g => g.status === 'approved' || (g.status === 'pending' && g.proposed_by?.id === user?.id))
const visibleGames = searchQuery // Apply proposer filter
? fuzzyFilter(baseGames, searchQuery, (g) => g.title + ' ' + (g.genre || '')) if (filterProposer !== 'all') {
: baseGames filteredGames = filteredGames.filter(g => g.proposed_by?.id === filterProposer)
}
return visibleGames.length === 0 ? ( // Apply challenges filter
if (filterChallenges === 'with') {
filteredGames = filteredGames.filter(g => {
const count = gameChallenges[g.id]?.length ?? g.challenges_count
return count > 0
})
} else if (filterChallenges === 'without') {
filteredGames = filteredGames.filter(g => {
const count = gameChallenges[g.id]?.length ?? g.challenges_count
return count === 0
})
}
// Apply search filter
if (searchQuery) {
filteredGames = fuzzyFilter(filteredGames, searchQuery, (g) => g.title + ' ' + (g.genre || ''))
}
const hasFilters = searchQuery || filterProposer !== 'all' || filterChallenges !== 'all'
return filteredGames.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-700 flex items-center justify-center"> <div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-700 flex items-center justify-center">
{searchQuery ? ( {hasFilters ? (
<Search className="w-8 h-8 text-gray-600" /> <Search className="w-8 h-8 text-gray-600" />
) : ( ) : (
<Gamepad2 className="w-8 h-8 text-gray-600" /> <Gamepad2 className="w-8 h-8 text-gray-600" />
)} )}
</div> </div>
<p className="text-gray-400"> <p className="text-gray-400">
{searchQuery {hasFilters
? `Ничего не найдено по запросу "${searchQuery}"` ? 'Ничего не найдено по заданным фильтрам'
: isOrganizer : isOrganizer
? 'Пока нет игр. Добавьте игры, чтобы начать!' ? 'Пока нет игр. Добавьте игры, чтобы начать!'
: 'Пока нет одобренных игр. Предложите свою!'} : 'Пока нет одобренных игр. Предложите свою!'}
@@ -1790,7 +1909,7 @@ export function LobbyPage() {
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{visibleGames.map((game) => renderGameCard(game, false))} {filteredGames.map((game) => renderGameCard(game, false))}
</div> </div>
) )
})()} })()}