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.
) : (
|
Order
|
Name
|
r.id)}
strategy={verticalListSortingStrategy}
>
{routes.map((route) => (
setEditing(r)}
/>
))}
)}
{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.
) : (
|
Order
|
Name
|
Type
|
Specialty
|
Section
|
Location
|
Lv Cap
|
Team
|
b.id)}
strategy={verticalListSortingStrategy}
>
{bosses.map((boss) => (
setEditingBoss(b)}
/>
))}
)}
>
)}
{/* 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}
/>
)
}