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:
119
frontend/src/components/admin/GameFormModal.tsx
Normal file
119
frontend/src/components/admin/GameFormModal.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { type FormEvent, useState, useEffect } from 'react'
|
||||
import { FormModal } from './FormModal'
|
||||
import type { Game, CreateGameInput, UpdateGameInput } from '../../types'
|
||||
|
||||
interface GameFormModalProps {
|
||||
game?: Game
|
||||
onSubmit: (data: CreateGameInput | UpdateGameInput) => void
|
||||
onClose: () => void
|
||||
isSubmitting?: boolean
|
||||
}
|
||||
|
||||
function slugify(name: string) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
|
||||
export function GameFormModal({ game, onSubmit, onClose, isSubmitting }: GameFormModalProps) {
|
||||
const [name, setName] = useState(game?.name ?? '')
|
||||
const [slug, setSlug] = useState(game?.slug ?? '')
|
||||
const [generation, setGeneration] = useState(String(game?.generation ?? ''))
|
||||
const [region, setRegion] = useState(game?.region ?? '')
|
||||
const [boxArtUrl, setBoxArtUrl] = useState(game?.boxArtUrl ?? '')
|
||||
const [releaseYear, setReleaseYear] = useState(game?.releaseYear ? String(game.releaseYear) : '')
|
||||
const [autoSlug, setAutoSlug] = useState(!game)
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSlug) setSlug(slugify(name))
|
||||
}, [name, autoSlug])
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
onSubmit({
|
||||
name,
|
||||
slug,
|
||||
generation: Number(generation),
|
||||
region,
|
||||
boxArtUrl: boxArtUrl || null,
|
||||
releaseYear: releaseYear ? Number(releaseYear) : null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<FormModal
|
||||
title={game ? 'Edit Game' : 'Add Game'}
|
||||
onClose={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
isSubmitting={isSubmitting}
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(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">Slug</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={slug}
|
||||
onChange={(e) => {
|
||||
setSlug(e.target.value)
|
||||
setAutoSlug(false)
|
||||
}}
|
||||
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">Generation</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
min={1}
|
||||
value={generation}
|
||||
onChange={(e) => setGeneration(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">Region</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={region}
|
||||
onChange={(e) => setRegion(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Box Art URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={boxArtUrl}
|
||||
onChange={(e) => setBoxArtUrl(e.target.value)}
|
||||
placeholder="Optional"
|
||||
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">Release Year</label>
|
||||
<input
|
||||
type="number"
|
||||
value={releaseYear}
|
||||
onChange={(e) => setReleaseYear(e.target.value)}
|
||||
placeholder="Optional"
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</FormModal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user