Seed all Pokemon species and add admin pagination
- Update fetch_pokeapi.py to import all 1025 Pokemon species instead of only those appearing in encounters - Add paginated response for /pokemon endpoint with total count - Add pagination controls to AdminPokemon page (First/Prev/Next/Last) - Show current page and total count in admin UI - Add bean for Pokemon forms support task - Update UX improvements bean with route grouping polish item Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
CreatePokemonInput,
|
||||
UpdatePokemonInput,
|
||||
BulkImportResult,
|
||||
PaginatedPokemon,
|
||||
CreateRouteEncounterInput,
|
||||
UpdateRouteEncounterInput,
|
||||
} from '../types'
|
||||
@@ -45,7 +46,7 @@ export const listPokemon = (search?: string, limit = 50, offset = 0) => {
|
||||
if (search) params.set('search', search)
|
||||
params.set('limit', String(limit))
|
||||
params.set('offset', String(offset))
|
||||
return api.get<Pokemon[]>(`/pokemon?${params}`)
|
||||
return api.get<PaginatedPokemon>(`/pokemon?${params}`)
|
||||
}
|
||||
|
||||
export const createPokemon = (data: CreatePokemonInput) =>
|
||||
|
||||
@@ -23,7 +23,8 @@ export function RouteEncounterFormModal({
|
||||
const [minLevel, setMinLevel] = useState(String(encounter?.minLevel ?? ''))
|
||||
const [maxLevel, setMaxLevel] = useState(String(encounter?.maxLevel ?? ''))
|
||||
|
||||
const { data: pokemonOptions = [] } = usePokemonList(search || undefined)
|
||||
const { data } = usePokemonList(search || undefined)
|
||||
const pokemonOptions = data?.items ?? []
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -14,10 +14,10 @@ import type {
|
||||
|
||||
// --- Queries ---
|
||||
|
||||
export function usePokemonList(search?: string) {
|
||||
export function usePokemonList(search?: string, limit = 50, offset = 0) {
|
||||
return useQuery({
|
||||
queryKey: ['pokemon', { search }],
|
||||
queryFn: () => adminApi.listPokemon(search),
|
||||
queryKey: ['pokemon', { search, limit, offset }],
|
||||
queryFn: () => adminApi.listPokemon(search, limit, offset),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,16 @@ import {
|
||||
} from '../../hooks/useAdmin'
|
||||
import type { Pokemon, CreatePokemonInput, UpdatePokemonInput } from '../../types'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export function AdminPokemon() {
|
||||
const [search, setSearch] = useState('')
|
||||
const { data: pokemon = [], isLoading } = usePokemonList(search || undefined)
|
||||
const [page, setPage] = useState(0)
|
||||
const offset = page * PAGE_SIZE
|
||||
const { data, isLoading } = usePokemonList(search || undefined, PAGE_SIZE, offset)
|
||||
const pokemon = data?.items ?? []
|
||||
const total = data?.total ?? 0
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE)
|
||||
const createPokemon = useCreatePokemon()
|
||||
const updatePokemon = useUpdatePokemon()
|
||||
const deletePokemon = useDeletePokemon()
|
||||
@@ -80,14 +87,20 @@ export function AdminPokemon() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-4 flex items-center gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
setPage(0) // Reset to first page on search
|
||||
}}
|
||||
placeholder="Search by name..."
|
||||
className="w-full max-w-sm px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{total} pokemon
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<AdminTable
|
||||
@@ -98,6 +111,48 @@ export function AdminPokemon() {
|
||||
keyFn={(p) => p.id}
|
||||
/>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(0)}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
First
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 px-2">
|
||||
Page {page + 1} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(totalPages - 1)}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<PokemonFormModal
|
||||
onSubmit={(data) =>
|
||||
|
||||
@@ -51,6 +51,13 @@ export interface BulkImportResult {
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export interface PaginatedPokemon {
|
||||
items: import('./game').Pokemon[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface CreateRouteEncounterInput {
|
||||
pokemonId: number
|
||||
encounterMethod: string
|
||||
|
||||
Reference in New Issue
Block a user