Add bulk import for evolutions, routes, and bosses

Add three new bulk import endpoints that accept the same JSON format as
their corresponding export endpoints, enabling round-trip compatibility:

- POST /evolutions/bulk-import (upsert by from/to pokemon pair)
- POST /games/{id}/routes/bulk-import (reuses seed loader for hierarchy)
- POST /games/{id}/bosses/bulk-import (reuses seed loader with team data)

Generalize BulkImportModal to support all entity types with configurable
title, example, and result labels. Wire up Bulk Import buttons on
AdminEvolutions, and AdminGameDetail routes/bosses tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 20:14:19 +01:00
parent 8e1c8b554f
commit 8f6d72a9c4
12 changed files with 373 additions and 15 deletions

View File

@@ -1,11 +1,13 @@
import { useState } from 'react'
import { AdminTable, type Column } from '../../components/admin/AdminTable'
import { BulkImportModal } from '../../components/admin/BulkImportModal'
import { EvolutionFormModal } from '../../components/admin/EvolutionFormModal'
import {
useEvolutionList,
useCreateEvolution,
useUpdateEvolution,
useDeleteEvolution,
useBulkImportEvolutions,
} from '../../hooks/useAdmin'
import { exportEvolutions } from '../../api/admin'
import { downloadJson } from '../../utils/download'
@@ -25,8 +27,10 @@ export function AdminEvolutions() {
const createEvolution = useCreateEvolution()
const updateEvolution = useUpdateEvolution()
const deleteEvolution = useDeleteEvolution()
const bulkImport = useBulkImportEvolutions()
const [showCreate, setShowCreate] = useState(false)
const [showBulkImport, setShowBulkImport] = useState(false)
const [editing, setEditing] = useState<EvolutionAdmin | null>(null)
const columns: Column<EvolutionAdmin>[] = [
@@ -71,6 +75,12 @@ export function AdminEvolutions() {
>
Export
</button>
<button
onClick={() => setShowBulkImport(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
>
Bulk Import
</button>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
@@ -146,6 +156,15 @@ export function AdminEvolutions() {
</div>
)}
{showBulkImport && (
<BulkImportModal
title="Bulk Import Evolutions"
example={`[\n { "from_pokeapi_id": 1, "to_pokeapi_id": 2, "trigger": "level-up", "min_level": 16 }\n]`}
onSubmit={(items) => bulkImport.mutateAsync(items)}
onClose={() => setShowBulkImport(false)}
/>
)}
{showCreate && (
<EvolutionFormModal
onSubmit={(data) =>

View File

@@ -16,6 +16,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { BulkImportModal } from '../../components/admin/BulkImportModal'
import { RouteFormModal } from '../../components/admin/RouteFormModal'
import { BossBattleFormModal } from '../../components/admin/BossBattleFormModal'
import { BossTeamEditor } from '../../components/admin/BossTeamEditor'
@@ -27,11 +28,13 @@ import {
useUpdateRoute,
useDeleteRoute,
useReorderRoutes,
useBulkImportRoutes,
useReorderBosses,
useCreateBossBattle,
useUpdateBossBattle,
useDeleteBossBattle,
useSetBossTeam,
useBulkImportBosses,
} from '../../hooks/useAdmin'
import { exportGameRoutes, exportGameBosses } from '../../api/admin'
import { downloadJson } from '../../utils/download'
@@ -149,16 +152,20 @@ export function AdminGameDetail() {
const updateRoute = useUpdateRoute(id)
const deleteRoute = useDeleteRoute(id)
const reorderRoutes = useReorderRoutes(id)
const bulkImportRoutes = useBulkImportRoutes(id)
const { data: bosses } = useGameBosses(id)
const createBoss = useCreateBossBattle(id)
const updateBoss = useUpdateBossBattle(id)
const deleteBoss = useDeleteBossBattle(id)
const reorderBosses = useReorderBosses(id)
const bulkImportBosses = useBulkImportBosses(id)
const [tab, setTab] = useState<'routes' | 'bosses'>('routes')
const [showCreate, setShowCreate] = useState(false)
const [showBulkImportRoutes, setShowBulkImportRoutes] = useState(false)
const [editing, setEditing] = useState<GameRoute | null>(null)
const [showCreateBoss, setShowCreateBoss] = useState(false)
const [showBulkImportBosses, setShowBulkImportBosses] = useState(false)
const [editingBoss, setEditingBoss] = useState<BossBattle | null>(null)
const [editingTeam, setEditingTeam] = useState<BossBattle | null>(null)
@@ -265,6 +272,12 @@ export function AdminGameDetail() {
>
Export
</button>
<button
onClick={() => setShowBulkImportRoutes(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
>
Bulk Import
</button>
<button
onClick={() => setShowCreate(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
@@ -273,6 +286,17 @@ export function AdminGameDetail() {
</button>
</div>
{showBulkImportRoutes && (
<BulkImportModal
title="Bulk Import Routes"
example={`[\n { "name": "Route 1", "order": 1, "encounters": [...], "children": [...] }\n]`}
createdLabel="Routes"
updatedLabel="Encounters"
onSubmit={(items) => bulkImportRoutes.mutateAsync(items)}
onClose={() => setShowBulkImportRoutes(false)}
/>
)}
{routes.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
No routes yet. Add one to get started.
@@ -365,6 +389,12 @@ export function AdminGameDetail() {
>
Export
</button>
<button
onClick={() => setShowBulkImportBosses(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
>
Bulk Import
</button>
<button
onClick={() => setShowCreateBoss(true)}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
@@ -373,6 +403,17 @@ export function AdminGameDetail() {
</button>
</div>
{showBulkImportBosses && (
<BulkImportModal
title="Bulk Import Boss Battles"
example={`[\n { "name": "Brock", "boss_type": "gym_leader", "level_cap": 15, "order": 1, "location": "Pewter City", "pokemon": [...] }\n]`}
createdLabel="Bosses"
updatedLabel="Updated"
onSubmit={(items) => bulkImportBosses.mutateAsync(items)}
onClose={() => setShowBulkImportBosses(false)}
/>
)}
{!bosses || bosses.length === 0 ? (
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
No boss battles yet. Add one to get started.

View File

@@ -158,7 +158,9 @@ export function AdminPokemon() {
{showBulkImport && (
<BulkImportModal
onSubmit={(items) => bulkImport.mutateAsync(items)}
title="Bulk Import Pokemon"
example={`[\n { "pokeapi_id": 1, "national_dex": 1, "name": "Bulbasaur", "types": ["Grass", "Poison"] }\n]`}
onSubmit={(items) => bulkImport.mutateAsync(items as Parameters<typeof bulkImport.mutateAsync>[0])}
onClose={() => setShowBulkImport(false)}
/>
)}