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:
134
frontend/src/components/admin/RouteEncounterFormModal.tsx
Normal file
134
frontend/src/components/admin/RouteEncounterFormModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user