Files
nuzlocke-tracker/frontend/src/components/BossDefeatModal.tsx
Julian Tabel 512be228a2 Auto-select boss team variant based on starter choice
When a run has a starter Pokemon, automatically match its species name
against boss battle condition labels (e.g., "charmander" matches "Chose
Charmander"). If exactly one variant matches, pre-select it and hide the
pill selector. Falls back to showing pills when no match is found.

Fixes starter lookup to use game routes data (which has encounterMethods
populated) instead of the run detail route (which defaults to empty).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 21:33:28 +01:00

174 lines
6.8 KiB
TypeScript

import { type FormEvent, useState, useMemo } from 'react'
import type { BossBattle, CreateBossResultInput } from '../types/game'
interface BossDefeatModalProps {
boss: BossBattle
onSubmit: (data: CreateBossResultInput) => void
onClose: () => void
isPending?: boolean
hardcoreMode?: boolean
starterName?: string | null
}
function matchVariant(labels: string[], starterName?: string | null): string | null {
if (!starterName || labels.length === 0) return null
const lower = starterName.toLowerCase()
const matches = labels.filter((l) => l.toLowerCase().includes(lower))
return matches.length === 1 ? matches[0] : null
}
export function BossDefeatModal({ boss, onSubmit, onClose, isPending, hardcoreMode, starterName }: BossDefeatModalProps) {
const [result, setResult] = useState<'won' | 'lost'>('won')
const [attempts, setAttempts] = useState('1')
const variantLabels = useMemo(() => {
const labels = new Set<string>()
for (const bp of boss.pokemon) {
if (bp.conditionLabel) labels.add(bp.conditionLabel)
}
return [...labels].sort()
}, [boss.pokemon])
const hasVariants = variantLabels.length > 0
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
const showPills = hasVariants && autoMatch === null
const [selectedVariant, setSelectedVariant] = useState<string | null>(
autoMatch ?? (hasVariants ? variantLabels[0] : null),
)
const displayedPokemon = useMemo(() => {
if (!hasVariants) return boss.pokemon
return boss.pokemon.filter(
(bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null,
)
}, [boss.pokemon, hasVariants, selectedVariant])
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
onSubmit({
bossBattleId: boss.id,
result: hardcoreMode ? 'won' : result,
attempts: hardcoreMode ? 1 : Number(attempts) || 1,
})
}
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-md w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold">Battle: {boss.name}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">{boss.location}</p>
</div>
{/* Boss team preview */}
{boss.pokemon.length > 0 && (
<div className="px-6 py-3 border-b border-gray-200 dark:border-gray-700">
{showPills && (
<div className="flex gap-1 mb-2 flex-wrap">
{variantLabels.map((label) => (
<button
key={label}
type="button"
onClick={() => setSelectedVariant(label)}
className={`px-2 py-0.5 text-xs font-medium rounded-full transition-colors ${
selectedVariant === label
? 'bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{label}
</button>
))}
</div>
)}
<div className="flex flex-wrap gap-3">
{[...displayedPokemon]
.sort((a, b) => a.order - b.order)
.map((bp) => (
<div key={bp.id} className="flex flex-col items-center">
{bp.pokemon.spriteUrl ? (
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-10 h-10" />
) : (
<div className="w-10 h-10 bg-gray-200 dark:bg-gray-700 rounded-full" />
)}
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{bp.pokemon.name}
</span>
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
Lv.{bp.level}
</span>
</div>
))}
</div>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="px-6 py-4 space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Result</label>
<div className="flex gap-2">
<button
type="button"
onClick={() => setResult('won')}
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
result === 'won'
? 'bg-green-600 text-white border-green-600'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
Won
</button>
{!hardcoreMode && (
<button
type="button"
onClick={() => setResult('lost')}
className={`flex-1 px-3 py-2 text-sm font-medium rounded-md border transition-colors ${
result === 'lost'
? 'bg-red-600 text-white border-red-600'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
>
Lost
</button>
)}
</div>
</div>
{!hardcoreMode && (
<div>
<label className="block text-sm font-medium mb-1">Attempts</label>
<input
type="number"
min={1}
value={attempts}
onChange={(e) => setAttempts(e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
</div>
)}
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
>
Cancel
</button>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? 'Saving...' : 'Save Result'}
</button>
</div>
</form>
</div>
</div>
)
}