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:
155
frontend/src/pages/admin/AdminRouteDetail.tsx
Normal file
155
frontend/src/pages/admin/AdminRouteDetail.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { AdminTable, type Column } from '../../components/admin/AdminTable'
|
||||
import { RouteEncounterFormModal } from '../../components/admin/RouteEncounterFormModal'
|
||||
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
|
||||
import { useGame, useRoutePokemon } from '../../hooks/useGames'
|
||||
import {
|
||||
useAddRouteEncounter,
|
||||
useUpdateRouteEncounter,
|
||||
useRemoveRouteEncounter,
|
||||
} from '../../hooks/useAdmin'
|
||||
import type {
|
||||
RouteEncounterDetail,
|
||||
CreateRouteEncounterInput,
|
||||
UpdateRouteEncounterInput,
|
||||
} from '../../types'
|
||||
|
||||
export function AdminRouteDetail() {
|
||||
const { gameId, routeId } = useParams<{ gameId: string; routeId: string }>()
|
||||
const gId = Number(gameId)
|
||||
const rId = Number(routeId)
|
||||
|
||||
const { data: game } = useGame(gId)
|
||||
const { data: encounters = [], isLoading } = useRoutePokemon(rId)
|
||||
|
||||
const addEncounter = useAddRouteEncounter(rId)
|
||||
const updateEncounter = useUpdateRouteEncounter(rId)
|
||||
const removeEncounter = useRemoveRouteEncounter(rId)
|
||||
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
const [editing, setEditing] = useState<RouteEncounterDetail | null>(null)
|
||||
const [deleting, setDeleting] = useState<RouteEncounterDetail | null>(null)
|
||||
|
||||
const route = game?.routes?.find((r) => r.id === rId)
|
||||
|
||||
const columns: Column<RouteEncounterDetail>[] = [
|
||||
{
|
||||
header: 'Pokemon',
|
||||
accessor: (e) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{e.pokemon.spriteUrl ? (
|
||||
<img src={e.pokemon.spriteUrl} alt={e.pokemon.name} className="w-6 h-6" />
|
||||
) : null}
|
||||
<span>
|
||||
#{e.pokemon.nationalDex} {e.pokemon.name}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ header: 'Method', accessor: (e) => e.encounterMethod },
|
||||
{ header: 'Rate', accessor: (e) => `${e.encounterRate}%` },
|
||||
{
|
||||
header: 'Levels',
|
||||
accessor: (e) =>
|
||||
e.minLevel === e.maxLevel ? `Lv ${e.minLevel}` : `Lv ${e.minLevel}-${e.maxLevel}`,
|
||||
},
|
||||
{
|
||||
header: 'Actions',
|
||||
accessor: (e) => (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setEditing(e)}
|
||||
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 text-sm"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleting(e)}
|
||||
className="text-red-600 hover:text-red-800 dark:text-red-400 text-sm"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
|
||||
<Link to="/admin/games" className="hover:underline">
|
||||
Games
|
||||
</Link>
|
||||
{' / '}
|
||||
<Link to={`/admin/games/${gId}`} className="hover:underline">
|
||||
{game?.name ?? '...'}
|
||||
</Link>
|
||||
{' / '}
|
||||
<span className="text-gray-900 dark:text-gray-100">
|
||||
{route?.name ?? '...'}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{route?.name ?? 'Route'} - Pokemon ({encounters.length})
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
Add Pokemon
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AdminTable
|
||||
columns={columns}
|
||||
data={encounters}
|
||||
isLoading={isLoading}
|
||||
emptyMessage="No pokemon assigned to this route yet."
|
||||
keyFn={(e) => e.id}
|
||||
/>
|
||||
|
||||
{showCreate && (
|
||||
<RouteEncounterFormModal
|
||||
onSubmit={(data) =>
|
||||
addEncounter.mutate(data as CreateRouteEncounterInput, {
|
||||
onSuccess: () => setShowCreate(false),
|
||||
})
|
||||
}
|
||||
onClose={() => setShowCreate(false)}
|
||||
isSubmitting={addEncounter.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<RouteEncounterFormModal
|
||||
encounter={editing}
|
||||
onSubmit={(data) =>
|
||||
updateEncounter.mutate(
|
||||
{ encounterId: editing.id, data: data as UpdateRouteEncounterInput },
|
||||
{ onSuccess: () => setEditing(null) },
|
||||
)
|
||||
}
|
||||
onClose={() => setEditing(null)}
|
||||
isSubmitting={updateEncounter.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleting && (
|
||||
<DeleteConfirmModal
|
||||
title={`Remove ${deleting.pokemon.name}?`}
|
||||
message="This will remove this pokemon from the route's encounter table."
|
||||
onConfirm={() =>
|
||||
removeEncounter.mutate(deleting.id, {
|
||||
onSuccess: () => setDeleting(null),
|
||||
})
|
||||
}
|
||||
onCancel={() => setDeleting(null)}
|
||||
isDeleting={removeEncounter.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user