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:
2026-02-08 20:29:55 +01:00
parent 5d444f0c91
commit c6521dd206
9 changed files with 188 additions and 20 deletions

View File

@@ -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)}
/>