import { useState, useEffect, useMemo } from 'react' import { useRoutePokemon } from '../hooks/useGames' import { EncounterMethodBadge, getMethodLabel, METHOD_ORDER, } from './EncounterMethodBadge' import type { Route, EncounterDetail, EncounterStatus, RouteEncounterDetail, } from '../types' interface EncounterModalProps { route: Route existing?: EncounterDetail dupedPokemonIds?: Set onSubmit: (data: { routeId: number pokemonId: number nickname?: string status: EncounterStatus catchLevel?: number }) => void onUpdate?: (data: { id: number data: { nickname?: string status?: EncounterStatus faintLevel?: number deathCause?: string } }) => void onClose: () => void isPending: boolean } const statusOptions: { value: EncounterStatus; label: string; color: string }[] = [ { value: 'caught', label: 'Caught', color: 'bg-green-100 text-green-800 border-green-300 dark:bg-green-900/40 dark:text-green-300 dark:border-green-700', }, { value: 'fainted', label: 'Fainted', color: 'bg-red-100 text-red-800 border-red-300 dark:bg-red-900/40 dark:text-red-300 dark:border-red-700', }, { value: 'missed', label: 'Missed / Ran', color: 'bg-gray-100 text-gray-800 border-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600', }, ] const SPECIAL_METHODS = ['starter', 'gift', 'fossil', 'trade'] function groupByMethod(pokemon: RouteEncounterDetail[]): { method: string; pokemon: RouteEncounterDetail[] }[] { const groups = new Map() for (const rp of pokemon) { const list = groups.get(rp.encounterMethod) ?? [] list.push(rp) groups.set(rp.encounterMethod, list) } return [...groups.entries()] .sort(([a], [b]) => { const ai = METHOD_ORDER.indexOf(a) const bi = METHOD_ORDER.indexOf(b) return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi) }) .map(([method, pokemon]) => ({ method, pokemon })) } export function EncounterModal({ route, existing, dupedPokemonIds, onSubmit, onUpdate, onClose, isPending, }: EncounterModalProps) { const { data: routePokemon, isLoading: loadingPokemon } = useRoutePokemon( route.id, ) const [selectedPokemon, setSelectedPokemon] = useState(null) const [status, setStatus] = useState( existing?.status ?? 'caught', ) const [nickname, setNickname] = useState(existing?.nickname ?? '') const [catchLevel, setCatchLevel] = useState( existing?.catchLevel?.toString() ?? '', ) const [faintLevel, setFaintLevel] = useState('') const [deathCause, setDeathCause] = useState('') const [search, setSearch] = useState('') const isEditing = !!existing // Pre-select pokemon when editing useEffect(() => { if (existing && routePokemon) { const match = routePokemon.find( (rp) => rp.pokemonId === existing.pokemonId, ) if (match) setSelectedPokemon(match) } }, [existing, routePokemon]) const filteredPokemon = routePokemon?.filter((rp) => rp.pokemon.name.toLowerCase().includes(search.toLowerCase()), ) const groupedPokemon = useMemo( () => (filteredPokemon ? groupByMethod(filteredPokemon) : []), [filteredPokemon], ) const hasMultipleGroups = groupedPokemon.length > 1 const handleSubmit = () => { if (isEditing && onUpdate) { onUpdate({ id: existing.id, data: { nickname: nickname || undefined, status, faintLevel: faintLevel ? Number(faintLevel) : undefined, deathCause: deathCause || undefined, }, }) } else if (selectedPokemon) { onSubmit({ routeId: route.id, pokemonId: selectedPokemon.pokemonId, nickname: nickname || undefined, status, catchLevel: catchLevel ? Number(catchLevel) : undefined, }) } } const canSubmit = isEditing || selectedPokemon return (

{isEditing ? 'Edit Encounter' : 'Log Encounter'}

{route.name}

{/* Pokemon Selection (only for new encounters) */} {!isEditing && (
{loadingPokemon ? (
) : filteredPokemon && filteredPokemon.length > 0 ? ( <> {(routePokemon?.length ?? 0) > 6 && ( setSearch(e.target.value)} className="w-full px-3 py-1.5 mb-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> )}
{groupedPokemon.map(({ method, pokemon }, groupIdx) => (
{groupIdx > 0 && (
)} {hasMultipleGroups && (
{getMethodLabel(method)}
)}
{pokemon.map((rp) => { const isDuped = dupedPokemonIds?.has(rp.pokemonId) ?? false return ( ) })}
))}
) : (

No pokemon data for this route

)}
)} {/* Editing: show pokemon info */} {isEditing && existing && (
{existing.pokemon.spriteUrl ? ( {existing.pokemon.name} ) : (
{existing.pokemon.name[0].toUpperCase()}
)}
{existing.pokemon.name}
Caught at Lv. {existing.catchLevel ?? '?'}
)} {/* Status */}
{statusOptions.map((opt) => ( ))}
{/* Nickname (for caught) */} {status === 'caught' && (
setNickname(e.target.value)} placeholder="Give it a name..." 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" />
)} {/* Level (for new caught encounters) */} {!isEditing && status === 'caught' && (
setCatchLevel(e.target.value)} placeholder={ selectedPokemon ? `${selectedPokemon.minLevel}–${selectedPokemon.maxLevel}` : 'Level' } className="w-24 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" />
)} {/* Faint Level + Death Cause (only when editing a caught pokemon to mark dead) */} {isEditing && existing?.status === 'caught' && existing?.faintLevel === null && ( <>
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" />
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" />
)}
) }