172 lines
5.7 KiB
TypeScript
172 lines
5.7 KiB
TypeScript
|
|
import { useState } from 'react'
|
||
|
|
import { useParams, useNavigate, Link } from 'react-router-dom'
|
||
|
|
import { AdminTable, type Column } from '../../components/admin/AdminTable'
|
||
|
|
import { RouteFormModal } from '../../components/admin/RouteFormModal'
|
||
|
|
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
|
||
|
|
import { useGame } from '../../hooks/useGames'
|
||
|
|
import {
|
||
|
|
useCreateRoute,
|
||
|
|
useUpdateRoute,
|
||
|
|
useDeleteRoute,
|
||
|
|
useReorderRoutes,
|
||
|
|
} from '../../hooks/useAdmin'
|
||
|
|
import type { Route as GameRoute, CreateRouteInput, UpdateRouteInput } from '../../types'
|
||
|
|
|
||
|
|
export function AdminGameDetail() {
|
||
|
|
const { gameId } = useParams<{ gameId: string }>()
|
||
|
|
const navigate = useNavigate()
|
||
|
|
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 [showCreate, setShowCreate] = useState(false)
|
||
|
|
const [editing, setEditing] = useState<GameRoute | null>(null)
|
||
|
|
const [deleting, setDeleting] = useState<GameRoute | null>(null)
|
||
|
|
|
||
|
|
if (isLoading) return <div className="py-8 text-center text-gray-500">Loading...</div>
|
||
|
|
if (!game) return <div className="py-8 text-center text-gray-500">Game not found</div>
|
||
|
|
|
||
|
|
const routes = game.routes ?? []
|
||
|
|
|
||
|
|
const moveRoute = (route: GameRoute, direction: 'up' | 'down') => {
|
||
|
|
const idx = routes.findIndex((r) => r.id === route.id)
|
||
|
|
if (direction === 'up' && idx <= 0) return
|
||
|
|
if (direction === 'down' && idx >= routes.length - 1) return
|
||
|
|
|
||
|
|
const swapIdx = direction === 'up' ? idx - 1 : idx + 1
|
||
|
|
const newRoutes = routes.map((r, i) => {
|
||
|
|
if (i === idx) return { id: r.id, order: routes[swapIdx].order }
|
||
|
|
if (i === swapIdx) return { id: r.id, order: routes[idx].order }
|
||
|
|
return { id: r.id, order: r.order }
|
||
|
|
})
|
||
|
|
reorderRoutes.mutate(newRoutes)
|
||
|
|
}
|
||
|
|
|
||
|
|
const columns: Column<GameRoute>[] = [
|
||
|
|
{ header: 'Order', accessor: (r) => r.order, className: 'w-16' },
|
||
|
|
{ header: 'Name', accessor: (r) => r.name },
|
||
|
|
{
|
||
|
|
header: 'Actions',
|
||
|
|
className: 'w-48',
|
||
|
|
accessor: (r) => {
|
||
|
|
const idx = routes.findIndex((rt) => rt.id === r.id)
|
||
|
|
return (
|
||
|
|
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
|
||
|
|
<button
|
||
|
|
onClick={() => moveRoute(r, 'up')}
|
||
|
|
disabled={idx === 0}
|
||
|
|
className="text-gray-600 hover:text-gray-800 dark:text-gray-400 disabled:opacity-30 text-sm"
|
||
|
|
title="Move up"
|
||
|
|
>
|
||
|
|
Up
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => moveRoute(r, 'down')}
|
||
|
|
disabled={idx === routes.length - 1}
|
||
|
|
className="text-gray-600 hover:text-gray-800 dark:text-gray-400 disabled:opacity-30 text-sm"
|
||
|
|
title="Move down"
|
||
|
|
>
|
||
|
|
Down
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => setEditing(r)}
|
||
|
|
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 text-sm"
|
||
|
|
>
|
||
|
|
Edit
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => setDeleting(r)}
|
||
|
|
className="text-red-600 hover:text-red-800 dark:text-red-400 text-sm"
|
||
|
|
>
|
||
|
|
Delete
|
||
|
|
</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>
|
||
|
|
{' / '}
|
||
|
|
<span className="text-gray-900 dark:text-gray-100">{game.name}</span>
|
||
|
|
</nav>
|
||
|
|
|
||
|
|
<div className="mb-6">
|
||
|
|
<h2 className="text-xl font-semibold">{game.name}</h2>
|
||
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
|
|
{game.region} · Gen {game.generation}
|
||
|
|
{game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex justify-between items-center mb-4">
|
||
|
|
<h3 className="text-lg font-medium">Routes ({routes.length})</h3>
|
||
|
|
<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 Route
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<AdminTable
|
||
|
|
columns={columns}
|
||
|
|
data={routes}
|
||
|
|
emptyMessage="No routes yet. Add one to get started."
|
||
|
|
onRowClick={(r) => navigate(`/admin/games/${id}/routes/${r.id}`)}
|
||
|
|
keyFn={(r) => r.id}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{showCreate && (
|
||
|
|
<RouteFormModal
|
||
|
|
nextOrder={routes.length > 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 && (
|
||
|
|
<RouteFormModal
|
||
|
|
route={editing}
|
||
|
|
onSubmit={(data) =>
|
||
|
|
updateRoute.mutate(
|
||
|
|
{ routeId: editing.id, data: data as UpdateRouteInput },
|
||
|
|
{ onSuccess: () => setEditing(null) },
|
||
|
|
)
|
||
|
|
}
|
||
|
|
onClose={() => setEditing(null)}
|
||
|
|
isSubmitting={updateRoute.isPending}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{deleting && (
|
||
|
|
<DeleteConfirmModal
|
||
|
|
title={`Delete ${deleting.name}?`}
|
||
|
|
message="This will permanently delete the route. Routes with existing encounters cannot be deleted."
|
||
|
|
onConfirm={() =>
|
||
|
|
deleteRoute.mutate(deleting.id, {
|
||
|
|
onSuccess: () => setDeleting(null),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
onCancel={() => setDeleting(null)}
|
||
|
|
isDeleting={deleteRoute.isPending}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|