Add filter controls to admin tables
Pokemon (type), Evolutions (trigger), Games (region/generation), and Runs (status/game) now have dropdown filters alongside search. Server-side filtering for paginated tables, client-side for small datasets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,26 @@ export function AdminRuns() {
|
||||
const deleteRun = useDeleteRun()
|
||||
|
||||
const [deleting, setDeleting] = useState<NuzlockeRun | null>(null)
|
||||
const [statusFilter, setStatusFilter] = useState('')
|
||||
const [gameFilter, setGameFilter] = useState('')
|
||||
|
||||
const gameMap = useMemo(
|
||||
() => new Map(games.map((g) => [g.id, g.name])),
|
||||
[games],
|
||||
)
|
||||
|
||||
const filteredRuns = useMemo(() => {
|
||||
let result = runs
|
||||
if (statusFilter) result = result.filter((r) => r.status === statusFilter)
|
||||
if (gameFilter) result = result.filter((r) => r.gameId === Number(gameFilter))
|
||||
return result
|
||||
}, [runs, statusFilter, gameFilter])
|
||||
|
||||
const runGames = useMemo(
|
||||
() => [...new Map(runs.map((r) => [r.gameId, gameMap.get(r.gameId) ?? `Game #${r.gameId}`])).entries()].sort((a, b) => a[1].localeCompare(b[1])),
|
||||
[runs, gameMap],
|
||||
)
|
||||
|
||||
const columns: Column<NuzlockeRun>[] = [
|
||||
{ header: 'Run Name', accessor: (r) => r.name, sortKey: (r) => r.name },
|
||||
{
|
||||
@@ -54,11 +68,45 @@ export function AdminRuns() {
|
||||
<h2 className="text-xl font-semibold">Runs</h2>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<option value="">All statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
<select
|
||||
value={gameFilter}
|
||||
onChange={(e) => setGameFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
>
|
||||
<option value="">All games</option>
|
||||
{runGames.map(([id, name]) => (
|
||||
<option key={id} value={id}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
{(statusFilter || gameFilter) && (
|
||||
<button
|
||||
onClick={() => { setStatusFilter(''); setGameFilter('') }}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{filteredRuns.length} runs
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AdminTable
|
||||
columns={columns}
|
||||
data={runs}
|
||||
data={filteredRuns}
|
||||
isLoading={runsLoading || gamesLoading}
|
||||
emptyMessage="No runs yet."
|
||||
emptyMessage="No runs found."
|
||||
keyFn={(r) => r.id}
|
||||
onRowClick={(r) => setDeleting(r)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user