Add pre-commit hooks for linting and formatting
All checks were successful
CI / backend-lint (push) Successful in 9s
CI / frontend-lint (push) Successful in 33s

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:
2026-02-14 16:41:24 +01:00
parent b05a75f7f2
commit 2963f16aa4
67 changed files with 1905 additions and 792 deletions

View File

@@ -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)}