Add pre-commit hooks for linting and formatting
Set up pre-commit framework with ruff (backend) and ESLint/Prettier/tsc (frontend) hooks to catch issues locally before CI. Auto-format all frontend files with Prettier to comply with the new check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,17 @@ import { type FormEvent, useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { EvolutionFormModal } from './EvolutionFormModal'
|
||||
import type { Pokemon, CreatePokemonInput, UpdatePokemonInput, EvolutionAdmin, UpdateEvolutionInput } from '../../types'
|
||||
import { usePokemonEncounterLocations, usePokemonEvolutionChain } from '../../hooks/usePokemon'
|
||||
import type {
|
||||
Pokemon,
|
||||
CreatePokemonInput,
|
||||
UpdatePokemonInput,
|
||||
EvolutionAdmin,
|
||||
UpdateEvolutionInput,
|
||||
} from '../../types'
|
||||
import {
|
||||
usePokemonEncounterLocations,
|
||||
usePokemonEvolutionChain,
|
||||
} from '../../hooks/usePokemon'
|
||||
import { useUpdateEvolution, useDeleteEvolution } from '../../hooks/useAdmin'
|
||||
import { formatEvolutionMethod } from '../../utils/formatEvolution'
|
||||
|
||||
@@ -18,20 +27,32 @@ interface PokemonFormModalProps {
|
||||
|
||||
type Tab = 'details' | 'evolutions' | 'encounters'
|
||||
|
||||
export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onDelete, isDeleting }: 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 [nationalDex, setNationalDex] = useState(
|
||||
String(pokemon?.nationalDex ?? '')
|
||||
)
|
||||
const [name, setName] = useState(pokemon?.name ?? '')
|
||||
const [types, setTypes] = useState(pokemon?.types.join(', ') ?? '')
|
||||
const [spriteUrl, setSpriteUrl] = useState(pokemon?.spriteUrl ?? '')
|
||||
const [activeTab, setActiveTab] = useState<Tab>('details')
|
||||
const [editingEvolution, setEditingEvolution] = useState<EvolutionAdmin | null>(null)
|
||||
const [editingEvolution, setEditingEvolution] =
|
||||
useState<EvolutionAdmin | null>(null)
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false)
|
||||
|
||||
const isEdit = !!pokemon
|
||||
const pokemonId = pokemon?.id ?? null
|
||||
const { data: encounterLocations, isLoading: encountersLoading } = usePokemonEncounterLocations(pokemonId)
|
||||
const { data: evolutionChain, isLoading: evolutionsLoading } = usePokemonEvolutionChain(pokemonId)
|
||||
const { data: encounterLocations, isLoading: encountersLoading } =
|
||||
usePokemonEncounterLocations(pokemonId)
|
||||
const { data: evolutionChain, isLoading: evolutionsLoading } =
|
||||
usePokemonEvolutionChain(pokemonId)
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const updateEvolution = useUpdateEvolution()
|
||||
@@ -42,7 +63,9 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
}, [onDelete])
|
||||
|
||||
const invalidateChain = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pokemon', pokemonId, 'evolution-chain'] })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['pokemon', pokemonId, 'evolution-chain'],
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
@@ -80,7 +103,9 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 shrink-0">
|
||||
<h2 className="text-lg font-semibold">{pokemon ? 'Edit Pokemon' : 'Add Pokemon'}</h2>
|
||||
<h2 className="text-lg font-semibold">
|
||||
{pokemon ? 'Edit Pokemon' : 'Add Pokemon'}
|
||||
</h2>
|
||||
{isEdit && (
|
||||
<div className="flex gap-1 mt-2">
|
||||
{tabs.map((tab) => (
|
||||
@@ -99,10 +124,15 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
|
||||
{/* Details tab (form) */}
|
||||
{activeTab === 'details' && (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col min-h-0 flex-1">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col min-h-0 flex-1"
|
||||
>
|
||||
<div className="px-6 py-4 space-y-4 overflow-y-auto">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">PokeAPI ID</label>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
PokeAPI ID
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
@@ -113,7 +143,9 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">National Dex #</label>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
National Dex #
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
@@ -134,7 +166,9 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Types (comma-separated)</label>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Types (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -145,7 +179,9 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Sprite URL</label>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
Sprite URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={spriteUrl}
|
||||
@@ -170,7 +206,11 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
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'}
|
||||
{isDeleting
|
||||
? 'Deleting...'
|
||||
: confirmingDelete
|
||||
? 'Confirm?'
|
||||
: 'Delete'}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
@@ -197,28 +237,35 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
<div className="flex flex-col min-h-0 flex-1">
|
||||
<div className="px-6 py-4 overflow-y-auto">
|
||||
{evolutionsLoading && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Loading...</p>
|
||||
)}
|
||||
{!evolutionsLoading && (!evolutionChain || evolutionChain.length === 0) && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No evolutions</p>
|
||||
)}
|
||||
{!evolutionsLoading && evolutionChain && evolutionChain.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{evolutionChain.map((evo) => (
|
||||
<button
|
||||
key={evo.id}
|
||||
type="button"
|
||||
onClick={() => setEditingEvolution(evo)}
|
||||
className="w-full text-left text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-2 py-1.5 -mx-2 transition-colors"
|
||||
>
|
||||
{evo.fromPokemon.name} → {evo.toPokemon.name}{' '}
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
({formatEvolutionMethod(evo)})
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading...
|
||||
</p>
|
||||
)}
|
||||
{!evolutionsLoading &&
|
||||
(!evolutionChain || evolutionChain.length === 0) && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No evolutions
|
||||
</p>
|
||||
)}
|
||||
{!evolutionsLoading &&
|
||||
evolutionChain &&
|
||||
evolutionChain.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
{evolutionChain.map((evo) => (
|
||||
<button
|
||||
key={evo.id}
|
||||
type="button"
|
||||
onClick={() => setEditingEvolution(evo)}
|
||||
className="w-full text-left text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded px-2 py-1.5 -mx-2 transition-colors"
|
||||
>
|
||||
{evo.fromPokemon.name} → {evo.toPokemon.name}{' '}
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
({formatEvolutionMethod(evo)})
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
|
||||
<button
|
||||
@@ -237,37 +284,48 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
<div className="flex flex-col min-h-0 flex-1">
|
||||
<div className="px-6 py-4 overflow-y-auto">
|
||||
{encountersLoading && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Loading...</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Loading...
|
||||
</p>
|
||||
)}
|
||||
{!encountersLoading && (!encounterLocations || encounterLocations.length === 0) && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No encounters</p>
|
||||
)}
|
||||
{!encountersLoading && encounterLocations && encounterLocations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{encounterLocations.map((game) => (
|
||||
<div key={game.gameId}>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{game.gameName}
|
||||
</div>
|
||||
<div className="space-y-0.5 pl-2">
|
||||
{game.encounters.map((enc, i) => (
|
||||
<div key={i} className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
|
||||
<Link
|
||||
to={`/admin/games/${game.gameId}/routes/${enc.routeId}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
{!encountersLoading &&
|
||||
(!encounterLocations || encounterLocations.length === 0) && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
No encounters
|
||||
</p>
|
||||
)}
|
||||
{!encountersLoading &&
|
||||
encounterLocations &&
|
||||
encounterLocations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{encounterLocations.map((game) => (
|
||||
<div key={game.gameId}>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{game.gameName}
|
||||
</div>
|
||||
<div className="space-y-0.5 pl-2">
|
||||
{game.encounters.map((enc, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1"
|
||||
>
|
||||
{enc.routeName}
|
||||
</Link>
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
— {enc.encounterMethod}, Lv. {enc.minLevel}–{enc.maxLevel}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<Link
|
||||
to={`/admin/games/${game.gameId}/routes/${enc.routeId}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{enc.routeName}
|
||||
</Link>
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
— {enc.encounterMethod}, Lv. {enc.minLevel}–
|
||||
{enc.maxLevel}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end shrink-0">
|
||||
<button
|
||||
@@ -294,7 +352,7 @@ export function PokemonFormModal({ pokemon, onSubmit, onClose, isSubmitting, onD
|
||||
setEditingEvolution(null)
|
||||
invalidateChain()
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
onClose={() => setEditingEvolution(null)}
|
||||
|
||||
Reference in New Issue
Block a user