2026-02-05 18:36:19 +01:00
|
|
|
import { useState } from 'react'
|
|
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
|
import { AdminTable, type Column } from '../../components/admin/AdminTable'
|
|
|
|
|
import { GameFormModal } from '../../components/admin/GameFormModal'
|
|
|
|
|
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
|
|
|
|
|
import { useGames } from '../../hooks/useGames'
|
|
|
|
|
import { useCreateGame, useUpdateGame, useDeleteGame } from '../../hooks/useAdmin'
|
2026-02-08 10:50:14 +01:00
|
|
|
import { exportGames } from '../../api/admin'
|
|
|
|
|
import { downloadJson } from '../../utils/download'
|
2026-02-05 18:36:19 +01:00
|
|
|
import type { Game, CreateGameInput, UpdateGameInput } from '../../types'
|
|
|
|
|
|
|
|
|
|
export function AdminGames() {
|
|
|
|
|
const navigate = useNavigate()
|
|
|
|
|
const { data: games = [], isLoading } = useGames()
|
|
|
|
|
const createGame = useCreateGame()
|
|
|
|
|
const updateGame = useUpdateGame()
|
|
|
|
|
const deleteGame = useDeleteGame()
|
|
|
|
|
|
|
|
|
|
const [showCreate, setShowCreate] = useState(false)
|
|
|
|
|
const [editing, setEditing] = useState<Game | null>(null)
|
|
|
|
|
const [deleting, setDeleting] = useState<Game | null>(null)
|
|
|
|
|
|
|
|
|
|
const columns: Column<Game>[] = [
|
|
|
|
|
{ header: 'Name', accessor: (g) => g.name },
|
|
|
|
|
{ header: 'Slug', accessor: (g) => g.slug },
|
Improve admin panel UX with toasts, evolution CRUD, sorting, drag-and-drop, and responsive layout
Add sonner toast notifications to all mutations, evolution management backend
(CRUD endpoints with search/pagination) and frontend (form modal with pokemon
selector, paginated list page), sortable AdminTable columns (Region/Gen/Year
on Games), drag-and-drop route reordering via @dnd-kit, skeleton loading states,
card-styled table wrappers, and responsive mobile nav in AdminLayout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 13:09:27 +01:00
|
|
|
{ header: 'Region', accessor: (g) => g.region, sortKey: (g) => g.region },
|
|
|
|
|
{ header: 'Gen', accessor: (g) => g.generation, sortKey: (g) => g.generation },
|
|
|
|
|
{ header: 'Year', accessor: (g) => g.releaseYear ?? '-', sortKey: (g) => g.releaseYear ?? 0 },
|
2026-02-05 18:36:19 +01:00
|
|
|
{
|
|
|
|
|
header: 'Actions',
|
|
|
|
|
accessor: (g) => (
|
|
|
|
|
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setEditing(g)}
|
|
|
|
|
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 text-sm"
|
|
|
|
|
>
|
|
|
|
|
Edit
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setDeleting(g)}
|
|
|
|
|
className="text-red-600 hover:text-red-800 dark:text-red-400 text-sm"
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex justify-between items-center mb-4">
|
|
|
|
|
<h2 className="text-xl font-semibold">Games</h2>
|
2026-02-08 10:50:14 +01:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
const data = await exportGames()
|
|
|
|
|
downloadJson(data, 'games.json')
|
|
|
|
|
}}
|
|
|
|
|
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"
|
|
|
|
|
>
|
|
|
|
|
Export
|
|
|
|
|
</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"
|
|
|
|
|
>
|
|
|
|
|
Add Game
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-02-05 18:36:19 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<AdminTable
|
|
|
|
|
columns={columns}
|
|
|
|
|
data={games}
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
emptyMessage="No games yet. Add one to get started."
|
|
|
|
|
onRowClick={(g) => navigate(`/admin/games/${g.id}`)}
|
|
|
|
|
keyFn={(g) => g.id}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{showCreate && (
|
|
|
|
|
<GameFormModal
|
|
|
|
|
onSubmit={(data) =>
|
|
|
|
|
createGame.mutate(data as CreateGameInput, {
|
|
|
|
|
onSuccess: () => setShowCreate(false),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
onClose={() => setShowCreate(false)}
|
|
|
|
|
isSubmitting={createGame.isPending}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{editing && (
|
|
|
|
|
<GameFormModal
|
|
|
|
|
game={editing}
|
|
|
|
|
onSubmit={(data) =>
|
|
|
|
|
updateGame.mutate(
|
|
|
|
|
{ id: editing.id, data: data as UpdateGameInput },
|
|
|
|
|
{ onSuccess: () => setEditing(null) },
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
onClose={() => setEditing(null)}
|
|
|
|
|
isSubmitting={updateGame.isPending}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{deleting && (
|
|
|
|
|
<DeleteConfirmModal
|
|
|
|
|
title={`Delete ${deleting.name}?`}
|
|
|
|
|
message="This will permanently delete the game and all its routes. Games with existing runs cannot be deleted."
|
|
|
|
|
onConfirm={() =>
|
|
|
|
|
deleteGame.mutate(deleting.id, {
|
|
|
|
|
onSuccess: () => setDeleting(null),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
onCancel={() => setDeleting(null)}
|
|
|
|
|
isDeleting={deleteGame.isPending}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|