import { useState } from 'react' import { useParams, Link } from 'react-router-dom' import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { SortableContext, sortableKeyboardCoordinates, useSortable, 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' import { TypeBadge } from '../../components/TypeBadge' import { useGame } from '../../hooks/useGames' import { useGameBosses } from '../../hooks/useBosses' import { useCreateRoute, useUpdateRoute, useDeleteRoute, useReorderRoutes, useBulkImportRoutes, useReorderBosses, useCreateBossBattle, useUpdateBossBattle, useDeleteBossBattle, useSetBossTeam, useBulkImportBosses, } from '../../hooks/useAdmin' import { exportGameRoutes, exportGameBosses } from '../../api/admin' import { downloadJson } from '../../utils/download' import type { Route as GameRoute, CreateRouteInput, UpdateRouteInput, BossBattle } from '../../types' import type { CreateBossBattleInput, UpdateBossBattleInput } from '../../types/admin' function SortableRouteRow({ route, onClick, }: { route: GameRoute onClick: (r: GameRoute) => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: route.id }) const style = { transform: CSS.Transform.toString(transform), transition, } return ( onClick(route)} > {route.order} {route.name} ) } function SortableBossRow({ boss, onClick, }: { boss: BossBattle onClick: (b: BossBattle) => void }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: boss.id }) const style = { transform: CSS.Transform.toString(transform), transition, } return ( onClick(boss)} > {boss.order} {boss.name} {boss.bossType.replace('_', ' ')} {boss.specialtyType ? : '\u2014'} {boss.section ?? '\u2014'} {boss.location} {boss.levelCap} {boss.pokemon.length} ) } export function AdminGameDetail() { const { gameId } = useParams<{ gameId: string }>() const id = Number(gameId) const { data: game, isLoading } = useGame(id) const createRoute = useCreateRoute(id) 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(null) const [showCreateBoss, setShowCreateBoss] = useState(false) const [showBulkImportBosses, setShowBulkImportBosses] = useState(false) const [editingBoss, setEditingBoss] = useState(null) const [editingTeam, setEditingTeam] = useState(null) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ) if (isLoading) return
Loading...
if (!game) return
Game not found
const routes = game.routes ?? [] const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return const oldIndex = routes.findIndex((r) => r.id === active.id) const newIndex = routes.findIndex((r) => r.id === over.id) if (oldIndex === -1 || newIndex === -1) return // Build new order assignments based on rearranged positions const reordered = [...routes] const [moved] = reordered.splice(oldIndex, 1) reordered.splice(newIndex, 0, moved) const newOrders = reordered.map((r, i) => ({ id: r.id, order: i + 1, })) reorderRoutes.mutate(newOrders) } const handleBossDragEnd = (event: DragEndEvent) => { if (!bosses) return const { active, over } = event if (!over || active.id === over.id) return const oldIndex = bosses.findIndex((b) => b.id === active.id) const newIndex = bosses.findIndex((b) => b.id === over.id) if (oldIndex === -1 || newIndex === -1) return const reordered = [...bosses] const [moved] = reordered.splice(oldIndex, 1) reordered.splice(newIndex, 0, moved) const newOrders = reordered.map((b, i) => ({ id: b.id, order: i + 1, })) reorderBosses.mutate(newOrders) } return (

{game.name}

{game.region.charAt(0).toUpperCase() + game.region.slice(1)} · Gen {game.generation} {game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''}

{tab === 'routes' && ( <>
{showBulkImportRoutes && ( bulkImportRoutes.mutateAsync(items)} onClose={() => setShowBulkImportRoutes(false)} /> )} {routes.length === 0 ? (
No routes yet. Add one to get started.
) : (
r.id)} strategy={verticalListSortingStrategy} > {routes.map((route) => ( setEditing(r)} /> ))}
Order Name
)} {showCreate && ( 0 ? Math.max(...routes.map((r) => r.order)) + 1 : 1} onSubmit={(data) => createRoute.mutate(data as CreateRouteInput, { onSuccess: () => setShowCreate(false), }) } onClose={() => setShowCreate(false)} isSubmitting={createRoute.isPending} /> )} {editing && ( updateRoute.mutate( { routeId: editing.id, data: data as UpdateRouteInput }, { onSuccess: () => setEditing(null) }, ) } onClose={() => setEditing(null)} isSubmitting={updateRoute.isPending} onDelete={() => deleteRoute.mutate(editing.id, { onSuccess: () => setEditing(null), }) } isDeleting={deleteRoute.isPending} detailUrl={`/admin/games/${id}/routes/${editing.id}`} /> )} )} {tab === 'bosses' && ( <>
{showBulkImportBosses && ( bulkImportBosses.mutateAsync(items)} onClose={() => setShowBulkImportBosses(false)} /> )} {!bosses || bosses.length === 0 ? (
No boss battles yet. Add one to get started.
) : (
b.id)} strategy={verticalListSortingStrategy} > {bosses.map((boss) => ( setEditingBoss(b)} /> ))}
Order Name Type Specialty Section Location Lv Cap Team
)} )} {/* Boss Battle Modals */} {showCreateBoss && ( b.order)) + 1 : 1} onSubmit={(data) => createBoss.mutate(data as CreateBossBattleInput, { onSuccess: () => setShowCreateBoss(false), }) } onClose={() => setShowCreateBoss(false)} isSubmitting={createBoss.isPending} /> )} {editingBoss && ( updateBoss.mutate( { bossId: editingBoss.id, data: data as UpdateBossBattleInput }, { onSuccess: () => setEditingBoss(null) }, ) } onClose={() => setEditingBoss(null)} isSubmitting={updateBoss.isPending} onDelete={() => deleteBoss.mutate(editingBoss.id, { onSuccess: () => setEditingBoss(null), }) } isDeleting={deleteBoss.isPending} onEditTeam={() => { setEditingTeam(editingBoss) setEditingBoss(null) }} /> )} {editingTeam && ( setEditingTeam(null)} /> )}
) } function BossTeamEditorWrapper({ gameId, boss, onClose, }: { gameId: number boss: BossBattle onClose: () => void }) { const setBossTeam = useSetBossTeam(gameId, boss.id) return ( setBossTeam.mutate(team, { onSuccess: onClose }) } onClose={onClose} isSaving={setBossTeam.isPending} /> ) }