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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import * as adminApi from '../api/admin'
|
||||
import type {
|
||||
CreateGameInput,
|
||||
@@ -10,6 +11,8 @@ import type {
|
||||
UpdatePokemonInput,
|
||||
CreateRouteEncounterInput,
|
||||
UpdateRouteEncounterInput,
|
||||
CreateEvolutionInput,
|
||||
UpdateEvolutionInput,
|
||||
} from '../types'
|
||||
|
||||
// --- Queries ---
|
||||
@@ -27,7 +30,11 @@ export function useCreateGame() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateGameInput) => adminApi.createGame(data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['games'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games'] })
|
||||
toast.success('Game created')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to create game: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,7 +43,11 @@ export function useUpdateGame() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateGameInput }) =>
|
||||
adminApi.updateGame(id, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['games'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games'] })
|
||||
toast.success('Game updated')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to update game: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,7 +55,11 @@ export function useDeleteGame() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => adminApi.deleteGame(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['games'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games'] })
|
||||
toast.success('Game deleted')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to delete game: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,7 +72,9 @@ export function useCreateRoute(gameId: number) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId] })
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] })
|
||||
toast.success('Route created')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to create route: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,7 +86,9 @@ export function useUpdateRoute(gameId: number) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId] })
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] })
|
||||
toast.success('Route updated')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to update route: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,7 +99,9 @@ export function useDeleteRoute(gameId: number) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId] })
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] })
|
||||
toast.success('Route deleted')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to delete route: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -91,7 +112,9 @@ export function useReorderRoutes(gameId: number) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId] })
|
||||
qc.invalidateQueries({ queryKey: ['games', gameId, 'routes'] })
|
||||
toast.success('Routes reordered')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to reorder routes: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,7 +124,11 @@ export function useCreatePokemon() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: CreatePokemonInput) => adminApi.createPokemon(data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pokemon'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['pokemon'] })
|
||||
toast.success('Pokemon created')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to create pokemon: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,7 +137,11 @@ export function useUpdatePokemon() {
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdatePokemonInput }) =>
|
||||
adminApi.updatePokemon(id, data),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pokemon'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['pokemon'] })
|
||||
toast.success('Pokemon updated')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to update pokemon: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,7 +149,11 @@ export function useDeletePokemon() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => adminApi.deletePokemon(id),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pokemon'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['pokemon'] })
|
||||
toast.success('Pokemon deleted')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to delete pokemon: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -127,7 +162,57 @@ export function useBulkImportPokemon() {
|
||||
return useMutation({
|
||||
mutationFn: (items: Array<{ nationalDex: number; name: string; types: string[]; spriteUrl?: string | null }>) =>
|
||||
adminApi.bulkImportPokemon(items),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['pokemon'] }),
|
||||
onSuccess: (result) => {
|
||||
qc.invalidateQueries({ queryKey: ['pokemon'] })
|
||||
toast.success(`Import complete: ${result.created} created, ${result.updated} updated`)
|
||||
},
|
||||
onError: (err) => toast.error(`Import failed: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
// --- Evolution Queries & Mutations ---
|
||||
|
||||
export function useEvolutionList(search?: string, limit = 50, offset = 0) {
|
||||
return useQuery({
|
||||
queryKey: ['evolutions', { search, limit, offset }],
|
||||
queryFn: () => adminApi.listEvolutions(search, limit, offset),
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateEvolution() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateEvolutionInput) => adminApi.createEvolution(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['evolutions'] })
|
||||
toast.success('Evolution created')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to create evolution: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
export function useUpdateEvolution() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateEvolutionInput }) =>
|
||||
adminApi.updateEvolution(id, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['evolutions'] })
|
||||
toast.success('Evolution updated')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to update evolution: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteEvolution() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => adminApi.deleteEvolution(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['evolutions'] })
|
||||
toast.success('Evolution deleted')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to delete evolution: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,8 +223,11 @@ export function useAddRouteEncounter(routeId: number) {
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateRouteEncounterInput) =>
|
||||
adminApi.addRouteEncounter(routeId, data),
|
||||
onSuccess: () =>
|
||||
qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] })
|
||||
toast.success('Encounter added')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to add encounter: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,8 +236,11 @@ export function useUpdateRouteEncounter(routeId: number) {
|
||||
return useMutation({
|
||||
mutationFn: ({ encounterId, data }: { encounterId: number; data: UpdateRouteEncounterInput }) =>
|
||||
adminApi.updateRouteEncounter(routeId, encounterId, data),
|
||||
onSuccess: () =>
|
||||
qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] })
|
||||
toast.success('Encounter updated')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to update encounter: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,7 +249,10 @@ export function useRemoveRouteEncounter(routeId: number) {
|
||||
return useMutation({
|
||||
mutationFn: (encounterId: number) =>
|
||||
adminApi.removeRouteEncounter(routeId, encounterId),
|
||||
onSuccess: () =>
|
||||
qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['routes', routeId, 'pokemon'] })
|
||||
toast.success('Encounter removed')
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to remove encounter: ${err.message}`),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user