Files
nuzlocke-tracker/frontend/src/pages/admin/AdminEvolutions.tsx
Julian Tabel f09b8213fd Add click-to-edit pattern across all admin tables
Replace Actions columns with clickable rows that open edit modals
directly. Delete is now an inline two-step confirm button in the
edit modal footer. Games modal links to routes/bosses detail,
route modal links to encounters, and boss modal has an Edit Team button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 13:44:38 +01:00

183 lines
6.3 KiB
TypeScript

import { useState } from 'react'
import { AdminTable, type Column } from '../../components/admin/AdminTable'
import { EvolutionFormModal } from '../../components/admin/EvolutionFormModal'
import {
useEvolutionList,
useCreateEvolution,
useUpdateEvolution,
useDeleteEvolution,
} from '../../hooks/useAdmin'
import { exportEvolutions } from '../../api/admin'
import { downloadJson } from '../../utils/download'
import type { EvolutionAdmin, CreateEvolutionInput, UpdateEvolutionInput } from '../../types'
const PAGE_SIZE = 50
export function AdminEvolutions() {
const [search, setSearch] = useState('')
const [page, setPage] = useState(0)
const offset = page * PAGE_SIZE
const { data, isLoading } = useEvolutionList(search || undefined, PAGE_SIZE, offset)
const evolutions = data?.items ?? []
const total = data?.total ?? 0
const totalPages = Math.ceil(total / PAGE_SIZE)
const createEvolution = useCreateEvolution()
const updateEvolution = useUpdateEvolution()
const deleteEvolution = useDeleteEvolution()
const [showCreate, setShowCreate] = useState(false)
const [editing, setEditing] = useState<EvolutionAdmin | null>(null)
const columns: Column<EvolutionAdmin>[] = [
{
header: 'From',
accessor: (e) => (
<div className="flex items-center gap-2">
{e.fromPokemon.spriteUrl && (
<img src={e.fromPokemon.spriteUrl} alt="" className="w-6 h-6" />
)}
<span>{e.fromPokemon.name}</span>
</div>
),
},
{
header: 'To',
accessor: (e) => (
<div className="flex items-center gap-2">
{e.toPokemon.spriteUrl && (
<img src={e.toPokemon.spriteUrl} alt="" className="w-6 h-6" />
)}
<span>{e.toPokemon.name}</span>
</div>
),
},
{ header: 'Trigger', accessor: (e) => e.trigger },
{ header: 'Level', accessor: (e) => e.minLevel ?? '-' },
{ header: 'Item', accessor: (e) => e.item ?? '-' },
]
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Evolutions</h2>
<div className="flex gap-2">
<button
onClick={async () => {
const data = await exportEvolutions()
downloadJson(data, 'evolutions.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 Evolution
</button>
</div>
</div>
<div className="mb-4 flex items-center gap-4">
<input
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value)
setPage(0)
}}
placeholder="Search by pokemon name, trigger, or item..."
className="w-full max-w-sm px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
<span className="text-sm text-gray-500 dark:text-gray-400">
{total} evolutions
</span>
</div>
<AdminTable
columns={columns}
data={evolutions}
isLoading={isLoading}
emptyMessage="No evolutions found."
keyFn={(e) => e.id}
onRowClick={(e) => setEditing(e)}
/>
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<div className="text-sm text-gray-500 dark:text-gray-400">
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setPage(0)}
disabled={page === 0}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
>
First
</button>
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
>
Prev
</button>
<span className="text-sm text-gray-600 dark:text-gray-300 px-2">
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
>
Next
</button>
<button
onClick={() => setPage(totalPages - 1)}
disabled={page >= totalPages - 1}
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
>
Last
</button>
</div>
</div>
)}
{showCreate && (
<EvolutionFormModal
onSubmit={(data) =>
createEvolution.mutate(data as CreateEvolutionInput, {
onSuccess: () => setShowCreate(false),
})
}
onClose={() => setShowCreate(false)}
isSubmitting={createEvolution.isPending}
/>
)}
{editing && (
<EvolutionFormModal
evolution={editing}
onSubmit={(data) =>
updateEvolution.mutate(
{ id: editing.id, data: data as UpdateEvolutionInput },
{ onSuccess: () => setEditing(null) },
)
}
onClose={() => setEditing(null)}
isSubmitting={updateEvolution.isPending}
onDelete={() =>
deleteEvolution.mutate(editing.id, {
onSuccess: () => setEditing(null),
})
}
isDeleting={deleteEvolution.isPending}
/>
)}
</div>
)
}