Add admin panel with CRUD endpoints and management UI

Add admin API endpoints for games, routes, pokemon, and route encounters
with full CRUD operations including bulk import. Build admin frontend
with game/route/pokemon management pages, navigation, and data tables.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 18:36:19 +01:00
parent a911259ef5
commit 55e6650e0e
28 changed files with 2140 additions and 10 deletions

View File

@@ -0,0 +1,134 @@
import { type FormEvent, useState } from 'react'
import { FormModal } from './FormModal'
import { usePokemonList } from '../../hooks/useAdmin'
import type { RouteEncounterDetail, CreateRouteEncounterInput, UpdateRouteEncounterInput } from '../../types'
interface RouteEncounterFormModalProps {
encounter?: RouteEncounterDetail
onSubmit: (data: CreateRouteEncounterInput | UpdateRouteEncounterInput) => void
onClose: () => void
isSubmitting?: boolean
}
export function RouteEncounterFormModal({
encounter,
onSubmit,
onClose,
isSubmitting,
}: RouteEncounterFormModalProps) {
const [search, setSearch] = useState('')
const [pokemonId, setPokemonId] = useState(encounter?.pokemonId ?? 0)
const [encounterMethod, setEncounterMethod] = useState(encounter?.encounterMethod ?? '')
const [encounterRate, setEncounterRate] = useState(String(encounter?.encounterRate ?? ''))
const [minLevel, setMinLevel] = useState(String(encounter?.minLevel ?? ''))
const [maxLevel, setMaxLevel] = useState(String(encounter?.maxLevel ?? ''))
const { data: pokemonOptions = [] } = usePokemonList(search || undefined)
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (encounter) {
onSubmit({
encounterMethod,
encounterRate: Number(encounterRate),
minLevel: Number(minLevel),
maxLevel: Number(maxLevel),
})
} else {
onSubmit({
pokemonId,
encounterMethod,
encounterRate: Number(encounterRate),
minLevel: Number(minLevel),
maxLevel: Number(maxLevel),
})
}
}
return (
<FormModal
title={encounter ? 'Edit Route Encounter' : 'Add Pokemon to Route'}
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
>
{!encounter && (
<div>
<label className="block text-sm font-medium mb-1">Pokemon</label>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search pokemon..."
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600 mb-2"
/>
{pokemonOptions.length > 0 && (
<select
required
value={pokemonId || ''}
onChange={(e) => setPokemonId(Number(e.target.value))}
size={Math.min(pokemonOptions.length, 6)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
>
{!pokemonId && <option value="">Select a pokemon...</option>}
{pokemonOptions.map((p) => (
<option key={p.id} value={p.id}>
#{p.nationalDex} {p.name} ({p.types.join('/')})
</option>
))}
</select>
)}
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">Encounter Method</label>
<input
type="text"
required
value={encounterMethod}
onChange={(e) => setEncounterMethod(e.target.value)}
placeholder="e.g. Walking, Surfing, Fishing"
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Encounter Rate (%)</label>
<input
type="number"
required
min={1}
max={100}
value={encounterRate}
onChange={(e) => setEncounterRate(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Min Level</label>
<input
type="number"
required
min={1}
max={100}
value={minLevel}
onChange={(e) => setMinLevel(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Max Level</label>
<input
type="number"
required
min={1}
max={100}
value={maxLevel}
onChange={(e) => setMaxLevel(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
</div>
</div>
</FormModal>
)
}