Filters
This commit is contained in:
@@ -125,6 +125,14 @@ export function LobbyPage() {
|
||||
const [searchQuery, setSearchQuery] = 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
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
|
||||
@@ -563,10 +571,6 @@ export function LobbyPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const selectAllGamesForGeneration = () => {
|
||||
setSelectedGamesForGeneration(approvedGames.map(g => g.id))
|
||||
}
|
||||
|
||||
const clearGameSelection = () => {
|
||||
setSelectedGamesForGeneration([])
|
||||
}
|
||||
@@ -644,6 +648,22 @@ export function LobbyPage() {
|
||||
const approvedGames = games.filter(g => g.status === 'approved')
|
||||
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) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
@@ -1422,6 +1442,8 @@ export function LobbyPage() {
|
||||
setShowGenerateSelection(false)
|
||||
clearGameSelection()
|
||||
setGenerateSearchQuery('')
|
||||
setGenerateFilterProposer('all')
|
||||
setGenerateFilterChallenges('all')
|
||||
}}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -1455,7 +1477,7 @@ export function LobbyPage() {
|
||||
{/* Game selection */}
|
||||
{showGenerateSelection && (
|
||||
<div className="space-y-3">
|
||||
{/* Search in generation */}
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<input
|
||||
@@ -1474,12 +1496,63 @@ export function LobbyPage() {
|
||||
</button>
|
||||
)}
|
||||
</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">
|
||||
<button
|
||||
onClick={selectAllGamesForGeneration}
|
||||
onClick={() => setSelectedGamesForGeneration(filteredGames.map(g => g.id))}
|
||||
className="text-neon-400 hover:text-neon-300 transition-colors"
|
||||
>
|
||||
Выбрать все
|
||||
Выбрать все ({filteredGames.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={clearGameSelection}
|
||||
@@ -1489,14 +1562,9 @@ export function LobbyPage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-2 max-h-64 overflow-y-auto custom-scrollbar">
|
||||
{(() => {
|
||||
const filteredGames = generateSearchQuery
|
||||
? fuzzyFilter(approvedGames, generateSearchQuery, (g) => g.title + ' ' + (g.genre || ''))
|
||||
: approvedGames
|
||||
|
||||
return filteredGames.length === 0 ? (
|
||||
{filteredGames.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-4 text-sm">
|
||||
Ничего не найдено по запросу "{generateSearchQuery}"
|
||||
Ничего не найдено
|
||||
</p>
|
||||
) : (
|
||||
filteredGames.map((game) => {
|
||||
@@ -1520,7 +1588,12 @@ export function LobbyPage() {
|
||||
{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>
|
||||
@@ -1528,10 +1601,12 @@ export function LobbyPage() {
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{generateMessage && (
|
||||
@@ -1702,8 +1777,9 @@ export function LobbyPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative mb-6">
|
||||
{/* Search and filters */}
|
||||
<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" />
|
||||
<input
|
||||
type="text"
|
||||
@@ -1721,6 +1797,28 @@ export function LobbyPage() {
|
||||
</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"
|
||||
>
|
||||
<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 */}
|
||||
{showAddGame && (
|
||||
@@ -1763,26 +1861,47 @@ export function LobbyPage() {
|
||||
|
||||
{/* Games */}
|
||||
{(() => {
|
||||
const baseGames = isOrganizer
|
||||
let filteredGames = isOrganizer
|
||||
? games.filter(g => g.status !== 'pending')
|
||||
: games.filter(g => g.status === 'approved' || (g.status === 'pending' && g.proposed_by?.id === user?.id))
|
||||
|
||||
const visibleGames = searchQuery
|
||||
? fuzzyFilter(baseGames, searchQuery, (g) => g.title + ' ' + (g.genre || ''))
|
||||
: baseGames
|
||||
// Apply proposer filter
|
||||
if (filterProposer !== 'all') {
|
||||
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="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" />
|
||||
) : (
|
||||
<Gamepad2 className="w-8 h-8 text-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-400">
|
||||
{searchQuery
|
||||
? `Ничего не найдено по запросу "${searchQuery}"`
|
||||
{hasFilters
|
||||
? 'Ничего не найдено по заданным фильтрам'
|
||||
: isOrganizer
|
||||
? 'Пока нет игр. Добавьте игры, чтобы начать!'
|
||||
: 'Пока нет одобренных игр. Предложите свою!'}
|
||||
@@ -1790,7 +1909,7 @@ export function LobbyPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{visibleGames.map((game) => renderGameCard(game, false))}
|
||||
{filteredGames.map((game) => renderGameCard(game, false))}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user