Files
nuzlocke-tracker/frontend/src/pages/admin/AdminGames.tsx

146 lines
5.0 KiB
TypeScript
Raw Normal View History

import { useState, useMemo } from 'react'
import { AdminTable, type Column } from '../../components/admin/AdminTable'
import { GameFormModal } from '../../components/admin/GameFormModal'
import { useGames } from '../../hooks/useGames'
import { useCreateGame, useUpdateGame, useDeleteGame } from '../../hooks/useAdmin'
import { exportGames } from '../../api/admin'
import { downloadJson } from '../../utils/download'
import type { Game, CreateGameInput, UpdateGameInput } from '../../types'
export function AdminGames() {
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 [regionFilter, setRegionFilter] = useState('')
const [genFilter, setGenFilter] = useState('')
const regions = useMemo(
() => [...new Set(games.map((g) => g.region))].sort(),
[games],
)
const generations = useMemo(
() => [...new Set(games.map((g) => g.generation))].sort((a, b) => a - b),
[games],
)
const filteredGames = useMemo(() => {
let result = games
if (regionFilter) result = result.filter((g) => g.region === regionFilter)
if (genFilter) result = result.filter((g) => g.generation === Number(genFilter))
return result
}, [games, regionFilter, genFilter])
const columns: Column<Game>[] = [
{ header: 'Name', accessor: (g) => g.name },
{ header: 'Slug', accessor: (g) => g.slug },
{ 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 },
]
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Games</h2>
<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>
</div>
<div className="mb-4 flex items-center gap-4">
<select
value={regionFilter}
onChange={(e) => setRegionFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
>
<option value="">All regions</option>
{regions.map((r) => (
<option key={r} value={r}>{r}</option>
))}
</select>
<select
value={genFilter}
onChange={(e) => setGenFilter(e.target.value)}
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
>
<option value="">All generations</option>
{generations.map((g) => (
<option key={g} value={g}>Gen {g}</option>
))}
</select>
{(regionFilter || genFilter) && (
<button
onClick={() => { setRegionFilter(''); setGenFilter('') }}
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
Clear filters
</button>
)}
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
{filteredGames.length} games
</span>
</div>
<AdminTable
columns={columns}
data={filteredGames}
isLoading={isLoading}
emptyMessage="No games found."
onRowClick={(g) => setEditing(g)}
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}
onDelete={() =>
deleteGame.mutate(editing.id, {
onSuccess: () => setEditing(null),
})
}
isDeleting={deleteGame.isPending}
detailUrl={`/admin/games/${editing.id}`}
/>
)}
</div>
)
}