Align repo config with global development standards
- Add missing tsconfig strictness flags (noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noPropertyAccessFromIndexSignature) and fix all resulting type errors - Replace ESLint/Prettier with oxlint 1.48.0 and oxfmt 0.33.0 - Pin all frontend and backend dependencies to exact versions - Pin GitHub Actions to SHA hashes with persist-credentials: false - Fix CI Python version mismatch (3.12 -> 3.14) and ruff target-version - Add vitest 4.0.18 with jsdom environment for frontend testing - Add ty 0.0.17 for Python type checking (non-blocking in CI) - Add actionlint and zizmor CI job for workflow linting and security audit - Add Dependabot config for npm, pip, and github-actions - Update CLAUDE.md and pre-commit hooks to reflect new tooling - Ignore Claude Code sandbox artifacts in gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,43 +1,36 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { useRoutePokemon } from '../hooks/useGames'
|
||||
import { useNameSuggestions } from '../hooks/useRuns'
|
||||
import {
|
||||
EncounterMethodBadge,
|
||||
getMethodLabel,
|
||||
METHOD_ORDER,
|
||||
} from './EncounterMethodBadge'
|
||||
import type {
|
||||
Route,
|
||||
EncounterDetail,
|
||||
EncounterStatus,
|
||||
RouteEncounterDetail,
|
||||
} from '../types'
|
||||
import { EncounterMethodBadge, getMethodLabel, METHOD_ORDER } from './EncounterMethodBadge'
|
||||
import type { Route, EncounterDetail, EncounterStatus, RouteEncounterDetail } from '../types'
|
||||
|
||||
interface EncounterModalProps {
|
||||
route: Route
|
||||
gameId: number
|
||||
runId: number
|
||||
namingScheme?: string | null
|
||||
isGenlocke?: boolean
|
||||
existing?: EncounterDetail
|
||||
dupedPokemonIds?: Set<number>
|
||||
retiredPokemonIds?: Set<number>
|
||||
namingScheme?: string | null | undefined
|
||||
isGenlocke?: boolean | undefined
|
||||
existing?: EncounterDetail | undefined
|
||||
dupedPokemonIds?: Set<number> | undefined
|
||||
retiredPokemonIds?: Set<number> | undefined
|
||||
onSubmit: (data: {
|
||||
routeId: number
|
||||
pokemonId: number
|
||||
nickname?: string
|
||||
nickname?: string | undefined
|
||||
status: EncounterStatus
|
||||
catchLevel?: number
|
||||
}) => void
|
||||
onUpdate?: (data: {
|
||||
id: number
|
||||
data: {
|
||||
nickname?: string
|
||||
status?: EncounterStatus
|
||||
faintLevel?: number
|
||||
deathCause?: string
|
||||
}
|
||||
catchLevel?: number | undefined
|
||||
}) => void
|
||||
onUpdate?:
|
||||
| ((data: {
|
||||
id: number
|
||||
data: {
|
||||
nickname?: string | undefined
|
||||
status?: EncounterStatus | undefined
|
||||
faintLevel?: number | undefined
|
||||
deathCause?: string | undefined
|
||||
}
|
||||
}) => void)
|
||||
| undefined
|
||||
onClose: () => void
|
||||
isPending: boolean
|
||||
}
|
||||
@@ -91,11 +84,9 @@ function pickRandomPokemon(
|
||||
pokemon: RouteEncounterDetail[],
|
||||
dupedIds?: Set<number>
|
||||
): RouteEncounterDetail | null {
|
||||
const eligible = dupedIds
|
||||
? pokemon.filter((rp) => !dupedIds.has(rp.pokemonId))
|
||||
: pokemon
|
||||
const eligible = dupedIds ? pokemon.filter((rp) => !dupedIds.has(rp.pokemonId)) : pokemon
|
||||
if (eligible.length === 0) return null
|
||||
return eligible[Math.floor(Math.random() * eligible.length)]
|
||||
return eligible[Math.floor(Math.random() * eligible.length)] ?? null
|
||||
}
|
||||
|
||||
export function EncounterModal({
|
||||
@@ -112,20 +103,12 @@ export function EncounterModal({
|
||||
onClose,
|
||||
isPending,
|
||||
}: EncounterModalProps) {
|
||||
const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon(
|
||||
route.id,
|
||||
gameId
|
||||
)
|
||||
const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon(route.id, gameId)
|
||||
|
||||
const [selectedPokemon, setSelectedPokemon] =
|
||||
useState<RouteEncounterDetail | null>(null)
|
||||
const [status, setStatus] = useState<EncounterStatus>(
|
||||
existing?.status ?? 'caught'
|
||||
)
|
||||
const [selectedPokemon, setSelectedPokemon] = useState<RouteEncounterDetail | null>(null)
|
||||
const [status, setStatus] = useState<EncounterStatus>(existing?.status ?? 'caught')
|
||||
const [nickname, setNickname] = useState(existing?.nickname ?? '')
|
||||
const [catchLevel, setCatchLevel] = useState<string>(
|
||||
existing?.catchLevel?.toString() ?? ''
|
||||
)
|
||||
const [catchLevel, setCatchLevel] = useState<string>(existing?.catchLevel?.toString() ?? '')
|
||||
const [faintLevel, setFaintLevel] = useState<string>('')
|
||||
const [deathCause, setDeathCause] = useState('')
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -133,8 +116,7 @@ export function EncounterModal({
|
||||
const isEditing = !!existing
|
||||
|
||||
const showSuggestions = !!namingScheme && status === 'caught' && !isEditing
|
||||
const lineagePokemonId =
|
||||
isGenlocke && selectedPokemon ? selectedPokemon.pokemonId : null
|
||||
const lineagePokemonId = isGenlocke && selectedPokemon ? selectedPokemon.pokemonId : null
|
||||
const {
|
||||
data: suggestions,
|
||||
refetch: regenerate,
|
||||
@@ -144,9 +126,7 @@ export function EncounterModal({
|
||||
// Pre-select pokemon when editing
|
||||
useEffect(() => {
|
||||
if (existing && routePokemon) {
|
||||
const match = routePokemon.find(
|
||||
(rp) => rp.pokemonId === existing.pokemonId
|
||||
)
|
||||
const match = routePokemon.find((rp) => rp.pokemonId === existing.pokemonId)
|
||||
if (match) setSelectedPokemon(match)
|
||||
}
|
||||
}, [existing, routePokemon])
|
||||
@@ -198,12 +178,7 @@ export function EncounterModal({
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -213,9 +188,7 @@ export function EncounterModal({
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{route.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{route.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
@@ -233,16 +206,12 @@ export function EncounterModal({
|
||||
loadingPokemon ||
|
||||
!routePokemon ||
|
||||
(dupedPokemonIds
|
||||
? routePokemon.every((rp) =>
|
||||
dupedPokemonIds.has(rp.pokemonId)
|
||||
)
|
||||
? routePokemon.every((rp) => dupedPokemonIds.has(rp.pokemonId))
|
||||
: false)
|
||||
}
|
||||
onClick={() => {
|
||||
if (routePokemon) {
|
||||
setSelectedPokemon(
|
||||
pickRandomPokemon(routePokemon, dupedPokemonIds)
|
||||
)
|
||||
setSelectedPokemon(pickRandomPokemon(routePokemon, dupedPokemonIds))
|
||||
}
|
||||
}}
|
||||
className="px-2.5 py-1 text-xs font-medium rounded-lg border border-purple-300 dark:border-purple-600 text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
@@ -279,15 +248,12 @@ export function EncounterModal({
|
||||
)}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{pokemon.map((rp) => {
|
||||
const isDuped =
|
||||
dupedPokemonIds?.has(rp.pokemonId) ?? false
|
||||
const isDuped = dupedPokemonIds?.has(rp.pokemonId) ?? false
|
||||
return (
|
||||
<button
|
||||
key={rp.id}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
!isDuped && setSelectedPokemon(rp)
|
||||
}
|
||||
onClick={() => !isDuped && setSelectedPokemon(rp)}
|
||||
disabled={isDuped}
|
||||
className={`flex flex-col items-center p-2 rounded-lg border text-center transition-colors ${
|
||||
isDuped
|
||||
@@ -305,7 +271,7 @@ export function EncounterModal({
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-xs font-bold">
|
||||
{rp.pokemon.name[0].toUpperCase()}
|
||||
{rp.pokemon.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300 mt-1 capitalize">
|
||||
@@ -318,19 +284,13 @@ export function EncounterModal({
|
||||
: 'already caught'}
|
||||
</span>
|
||||
)}
|
||||
{!isDuped &&
|
||||
SPECIAL_METHODS.includes(
|
||||
rp.encounterMethod
|
||||
) && (
|
||||
<EncounterMethodBadge
|
||||
method={rp.encounterMethod}
|
||||
/>
|
||||
)}
|
||||
{!isDuped && SPECIAL_METHODS.includes(rp.encounterMethod) && (
|
||||
<EncounterMethodBadge method={rp.encounterMethod} />
|
||||
)}
|
||||
{!isDuped && (
|
||||
<span className="text-[10px] text-gray-400">
|
||||
Lv. {rp.minLevel}
|
||||
{rp.maxLevel !== rp.minLevel &&
|
||||
`–${rp.maxLevel}`}
|
||||
{rp.maxLevel !== rp.minLevel && `–${rp.maxLevel}`}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -360,7 +320,7 @@ export function EncounterModal({
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-lg font-bold">
|
||||
{existing.pokemon.name[0].toUpperCase()}
|
||||
{existing.pokemon.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
@@ -477,53 +437,45 @@ export function EncounterModal({
|
||||
)}
|
||||
|
||||
{/* Faint Level + Death Cause (only when editing a caught pokemon to mark dead) */}
|
||||
{isEditing &&
|
||||
existing?.status === 'caught' &&
|
||||
existing?.faintLevel === null && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="faint-level"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Faint Level{' '}
|
||||
<span className="font-normal text-gray-400">
|
||||
(mark as dead)
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="faint-level"
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={faintLevel}
|
||||
onChange={(e) => setFaintLevel(e.target.value)}
|
||||
placeholder="Leave empty if still alive"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="death-cause"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Cause of Death{' '}
|
||||
<span className="font-normal text-gray-400">
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="death-cause"
|
||||
type="text"
|
||||
maxLength={100}
|
||||
value={deathCause}
|
||||
onChange={(e) => setDeathCause(e.target.value)}
|
||||
placeholder="e.g. Crit from rival's Charizard"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isEditing && existing?.status === 'caught' && existing?.faintLevel === null && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="faint-level"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Faint Level <span className="font-normal text-gray-400">(mark as dead)</span>
|
||||
</label>
|
||||
<input
|
||||
id="faint-level"
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
value={faintLevel}
|
||||
onChange={(e) => setFaintLevel(e.target.value)}
|
||||
placeholder="Leave empty if still alive"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
htmlFor="death-cause"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
>
|
||||
Cause of Death <span className="font-normal text-gray-400">(optional)</span>
|
||||
</label>
|
||||
<input
|
||||
id="death-cause"
|
||||
type="text"
|
||||
maxLength={100}
|
||||
value={deathCause}
|
||||
onChange={(e) => setDeathCause(e.target.value)}
|
||||
placeholder="e.g. Crit from rival's Charizard"
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-6 py-4 rounded-b-xl flex justify-end gap-3">
|
||||
|
||||
Reference in New Issue
Block a user