Add export buttons to all admin panel screens

Backend export endpoints return DB data in seed JSON format
(games, routes+encounters, pokemon, evolutions). Frontend
downloads the JSON via new Export buttons on each admin page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 10:50:14 +01:00
parent 8fbf658a27
commit 5cdcd149b6
9 changed files with 236 additions and 21 deletions

View File

@@ -83,6 +83,19 @@ export const updateEvolution = (id: number, data: UpdateEvolutionInput) =>
export const deleteEvolution = (id: number) =>
api.del(`/evolutions/${id}`)
// Export
export const exportGames = () =>
api.get<Record<string, unknown>[]>('/export/games')
export const exportGameRoutes = (gameId: number) =>
api.get<{ filename: string; data: unknown }>(`/export/games/${gameId}/routes`)
export const exportPokemon = () =>
api.get<Record<string, unknown>[]>('/export/pokemon')
export const exportEvolutions = () =>
api.get<Record<string, unknown>[]>('/export/evolutions')
// Route Encounters
export const addRouteEncounter = (routeId: number, data: CreateRouteEncounterInput) =>
api.post<RouteEncounterDetail>(`/routes/${routeId}/pokemon`, data)

View File

@@ -8,6 +8,8 @@ import {
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
@@ -80,12 +82,23 @@ export function AdminEvolutions() {
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Evolutions</h2>
<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 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">

View File

@@ -1,5 +1,4 @@
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams, useNavigate, Link } from 'react-router-dom'
import {
DndContext,
@@ -26,6 +25,8 @@ import {
useDeleteRoute,
useReorderRoutes,
} from '../../hooks/useAdmin'
import { exportGameRoutes } from '../../api/admin'
import { downloadJson } from '../../utils/download'
import type { Route as GameRoute, CreateRouteInput, UpdateRouteInput } from '../../types'
function SortableRouteRow({
@@ -161,14 +162,13 @@ export function AdminGameDetail() {
<h3 className="text-lg font-medium">Routes ({routes.length})</h3>
<div className="flex gap-2">
<button
onClick={() => {
const names = routes.map((r) => r.name)
navigator.clipboard.writeText(JSON.stringify(names, null, 2))
toast.success('Route order copied to clipboard')
onClick={async () => {
const result = await exportGameRoutes(id)
downloadJson(result.data, result.filename)
}}
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 Order
Export
</button>
<button
onClick={() => setShowCreate(true)}

View File

@@ -5,6 +5,8 @@ import { GameFormModal } from '../../components/admin/GameFormModal'
import { DeleteConfirmModal } from '../../components/admin/DeleteConfirmModal'
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() {
@@ -49,12 +51,23 @@ export function AdminGames() {
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Games</h2>
<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 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>
<AdminTable

View File

@@ -10,6 +10,8 @@ import {
useDeletePokemon,
useBulkImportPokemon,
} from '../../hooks/useAdmin'
import { exportPokemon } from '../../api/admin'
import { downloadJson } from '../../utils/download'
import type { Pokemon, CreatePokemonInput, UpdatePokemonInput } from '../../types'
const PAGE_SIZE = 50
@@ -72,6 +74,15 @@ export function AdminPokemon() {
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Pokemon</h2>
<div className="flex gap-2">
<button
onClick={async () => {
const data = await exportPokemon()
downloadJson(data, 'pokemon.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={() => setShowBulkImport(true)}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"

View File

@@ -0,0 +1,11 @@
export function downloadJson(data: unknown, filename: string) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}