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>
This commit is contained in:
2026-02-08 13:44:38 +01:00
parent 76d69dfaf1
commit f09b8213fd
14 changed files with 145 additions and 228 deletions

View File

@@ -10,6 +10,9 @@ interface BossBattleFormModalProps {
onSubmit: (data: CreateBossBattleInput | UpdateBossBattleInput) => void
onClose: () => void
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
onEditTeam?: () => void
}
const BOSS_TYPES = [
@@ -28,6 +31,9 @@ export function BossBattleFormModal({
onSubmit,
onClose,
isSubmitting,
onDelete,
isDeleting,
onEditTeam,
}: BossBattleFormModalProps) {
const [name, setName] = useState(boss?.name ?? '')
const [bossType, setBossType] = useState(boss?.bossType ?? 'gym_leader')
@@ -63,6 +69,17 @@ export function BossBattleFormModal({
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
onDelete={onDelete}
isDeleting={isDeleting}
headerExtra={onEditTeam ? (
<button
type="button"
onClick={onEditTeam}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
Edit Team ({boss?.pokemon.length ?? 0})
</button>
) : undefined}
>
<div className="grid grid-cols-2 gap-4">
<div>

View File

@@ -8,6 +8,8 @@ interface EvolutionFormModalProps {
onSubmit: (data: CreateEvolutionInput | UpdateEvolutionInput) => void
onClose: () => void
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
}
const TRIGGER_OPTIONS = ['level-up', 'trade', 'use-item', 'shed', 'other']
@@ -17,6 +19,8 @@ export function EvolutionFormModal({
onSubmit,
onClose,
isSubmitting,
onDelete,
isDeleting,
}: EvolutionFormModalProps) {
const [fromPokemonId, setFromPokemonId] = useState<number | null>(
evolution?.fromPokemonId ?? null,
@@ -52,6 +56,8 @@ export function EvolutionFormModal({
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
onDelete={onDelete}
isDeleting={isDeleting}
>
<PokemonSelector
label="From Pokemon"

View File

@@ -1,4 +1,4 @@
import { type FormEvent, type ReactNode } from 'react'
import { type FormEvent, type ReactNode, useState, useEffect } from 'react'
interface FormModalProps {
title: string
@@ -7,6 +7,9 @@ interface FormModalProps {
children: ReactNode
submitLabel?: string
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
headerExtra?: ReactNode
}
export function FormModal({
@@ -16,17 +19,46 @@ export function FormModal({
children,
submitLabel = 'Save',
isSubmitting,
onDelete,
isDeleting,
headerExtra,
}: FormModalProps) {
const [confirmingDelete, setConfirmingDelete] = useState(false)
// Reset confirm state when modal closes/reopens
useEffect(() => {
setConfirmingDelete(false)
}, [onDelete])
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold">{title}</h2>
{headerExtra}
</div>
<form onSubmit={onSubmit}>
<div className="px-6 py-4 space-y-4">{children}</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex items-center gap-3">
{onDelete && (
<button
type="button"
disabled={isDeleting}
onClick={() => {
if (confirmingDelete) {
onDelete()
} else {
setConfirmingDelete(true)
}
}}
onBlur={() => setConfirmingDelete(false)}
className="px-4 py-2 text-sm font-medium rounded-md text-red-600 dark:text-red-400 border border-red-300 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 disabled:opacity-50"
>
{isDeleting ? 'Deleting...' : confirmingDelete ? 'Confirm?' : 'Delete'}
</button>
)}
<div className="flex-1" />
<button
type="button"
onClick={onClose}

View File

@@ -1,4 +1,5 @@
import { type FormEvent, useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { FormModal } from './FormModal'
import type { Game, CreateGameInput, UpdateGameInput } from '../../types'
@@ -7,6 +8,9 @@ interface GameFormModalProps {
onSubmit: (data: CreateGameInput | UpdateGameInput) => void
onClose: () => void
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
detailUrl?: string
}
function slugify(name: string) {
@@ -16,7 +20,7 @@ function slugify(name: string) {
.replace(/^-|-$/g, '')
}
export function GameFormModal({ game, onSubmit, onClose, isSubmitting }: GameFormModalProps) {
export function GameFormModal({ game, onSubmit, onClose, isSubmitting, onDelete, isDeleting, detailUrl }: GameFormModalProps) {
const [name, setName] = useState(game?.name ?? '')
const [slug, setSlug] = useState(game?.slug ?? '')
const [generation, setGeneration] = useState(String(game?.generation ?? ''))
@@ -47,6 +51,16 @@ export function GameFormModal({ game, onSubmit, onClose, isSubmitting }: GameFor
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
onDelete={onDelete}
isDeleting={isDeleting}
headerExtra={detailUrl ? (
<Link
to={detailUrl}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
View Routes & Bosses
</Link>
) : undefined}
>
<div>
<label className="block text-sm font-medium mb-1">Name</label>

View File

@@ -7,9 +7,11 @@ interface PokemonFormModalProps {
onSubmit: (data: CreatePokemonInput | UpdatePokemonInput) => void
onClose: () => void
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
}
export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting }: PokemonFormModalProps) {
export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onDelete, isDeleting }: PokemonFormModalProps) {
const [pokeapiId, setPokeapiId] = useState(String(pokemon?.pokeapiId ?? ''))
const [nationalDex, setNationalDex] = useState(String(pokemon?.nationalDex ?? ''))
const [name, setName] = useState(pokemon?.name ?? '')
@@ -37,6 +39,8 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting }: P
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
onDelete={onDelete}
isDeleting={isDeleting}
>
<div>
<label className="block text-sm font-medium mb-1">PokeAPI ID</label>

View File

@@ -8,6 +8,8 @@ interface RouteEncounterFormModalProps {
onSubmit: (data: CreateRouteEncounterInput | UpdateRouteEncounterInput) => void
onClose: () => void
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
}
export function RouteEncounterFormModal({
@@ -15,6 +17,8 @@ export function RouteEncounterFormModal({
onSubmit,
onClose,
isSubmitting,
onDelete,
isDeleting,
}: RouteEncounterFormModalProps) {
const [search, setSearch] = useState('')
const [pokemonId, setPokemonId] = useState(encounter?.pokemonId ?? 0)
@@ -52,6 +56,8 @@ export function RouteEncounterFormModal({
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
onDelete={onDelete}
isDeleting={isDeleting}
>
{!encounter && (
<div>

View File

@@ -1,4 +1,5 @@
import { type FormEvent, useState } from 'react'
import { Link } from 'react-router-dom'
import { FormModal } from './FormModal'
import type { Route, CreateRouteInput, UpdateRouteInput } from '../../types'
@@ -8,9 +9,12 @@ interface RouteFormModalProps {
onSubmit: (data: CreateRouteInput | UpdateRouteInput) => void
onClose: () => void
isSubmitting?: boolean
onDelete?: () => void
isDeleting?: boolean
detailUrl?: string
}
export function RouteFormModal({ route, nextOrder, onSubmit, onClose, isSubmitting }: RouteFormModalProps) {
export function RouteFormModal({ route, nextOrder, onSubmit, onClose, isSubmitting, onDelete, isDeleting, detailUrl }: RouteFormModalProps) {
const [name, setName] = useState(route?.name ?? '')
const [order, setOrder] = useState(String(route?.order ?? nextOrder ?? 0))
const [pinwheelZone, setPinwheelZone] = useState(
@@ -32,6 +36,16 @@ export function RouteFormModal({ route, nextOrder, onSubmit, onClose, isSubmitti
onClose={onClose}
onSubmit={handleSubmit}
isSubmitting={isSubmitting}
onDelete={onDelete}
isDeleting={isDeleting}
headerExtra={detailUrl ? (
<Link
to={detailUrl}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
View Encounters
</Link>
) : undefined}
>
<div>
<label className="block text-sm font-medium mb-1">Name</label>