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,12 +1496,63 @@ export function LobbyPage() {
</button> </button>
)} )}
</div> </div>
{/* Filters */}
<div className="flex gap-2">
<select
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>
{uniqueProposers.map(u => (
<option key={u.id} value={u.id}>{u.nickname}</option>
))}
</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>
<option value="with">С заданиями</option>
<option value="without">Без заданий</option>
</select>
</div>
{(() => {
// Compute filtered games
let filteredGames = approvedGames
// Apply proposer filter
if (generateFilterProposer !== 'all') {
filteredGames = filteredGames.filter(g => g.proposed_by?.id === generateFilterProposer)
}
// Apply challenges filter
if (generateFilterChallenges === 'with') {
filteredGames = filteredGames.filter(g => {
const count = gameChallenges[g.id]?.length ?? g.challenges_count
return count > 0
})
} else if (generateFilterChallenges === 'without') {
filteredGames = filteredGames.filter(g => {
const count = gameChallenges[g.id]?.length ?? g.challenges_count
return count === 0
})
}
// Apply search filter
if (generateSearchQuery) {
filteredGames = fuzzyFilter(filteredGames, generateSearchQuery, (g) => g.title + ' ' + (g.genre || ''))
}
return (
<>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<button <button
onClick={selectAllGamesForGeneration} onClick={() => setSelectedGamesForGeneration(filteredGames.map(g => g.id))}
className="text-neon-400 hover:text-neon-300 transition-colors" className="text-neon-400 hover:text-neon-300 transition-colors"
> >
Выбрать все Выбрать все ({filteredGames.length})
</button> </button>
<button <button
onClick={clearGameSelection} onClick={clearGameSelection}
@@ -1489,14 +1562,9 @@ export function LobbyPage() {
</button> </button>
</div> </div>
<div className="grid gap-2 max-h-64 overflow-y-auto custom-scrollbar"> <div className="grid gap-2 max-h-64 overflow-y-auto custom-scrollbar">
{(() => { {filteredGames.length === 0 ? (
const filteredGames = generateSearchQuery
? fuzzyFilter(approvedGames, generateSearchQuery, (g) => g.title + ' ' + (g.genre || ''))
: approvedGames
return filteredGames.length === 0 ? (
<p className="text-center text-gray-500 py-4 text-sm"> <p className="text-center text-gray-500 py-4 text-sm">
Ничего не найдено по запросу "{generateSearchQuery}" Ничего не найдено
</p> </p>
) : ( ) : (
filteredGames.map((game) => { filteredGames.map((game) => {
@@ -1520,7 +1588,12 @@ export function LobbyPage() {
{isSelected && <Check className="w-3 h-3 text-white" />} {isSelected && <Check className="w-3 h-3 text-white" />}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="text-white font-medium truncate">{game.title}</p> <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"> <p className="text-xs text-gray-400">
{challengeCount > 0 ? `${challengeCount} заданий` : 'Нет заданий'} {challengeCount > 0 ? `${challengeCount} заданий` : 'Нет заданий'}
</p> </p>
@@ -1528,10 +1601,12 @@ export function LobbyPage() {
</button> </button>
) )
}) })
)}
</div>
</>
) )
})()} })()}
</div> </div>
</div>
)} )}
{generateMessage && ( {generateMessage && (
@@ -1702,8 +1777,9 @@ export function LobbyPage() {
)} )}
</div> </div>
{/* Search */} {/* Search and filters */}
<div className="relative mb-6"> <div className="space-y-3 mb-6">
<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
type="text" type="text"
@@ -1721,6 +1797,28 @@ export function LobbyPage() {
</button> </button>
)} )}
</div> </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"
>
<option value="all">Все участники</option>
{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>
{/* Add game form */} {/* Add game form */}
{showAddGame && ( {showAddGame && (
@@ -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>
) )
})()} })()}