2026-02-08 21:20:30 +01:00
|
|
|
import { type FormEvent, useState, useMemo } from 'react'
|
2026-02-08 11:16:13 +01:00
|
|
|
import type { BossBattle, CreateBossResultInput } from '../types/game'
|
2026-02-16 21:17:32 +01:00
|
|
|
import { ConditionBadge } from './ConditionBadge'
|
2026-02-08 11:16:13 +01:00
|
|
|
|
|
|
|
|
interface BossDefeatModalProps {
|
|
|
|
|
boss: BossBattle
|
|
|
|
|
onSubmit: (data: CreateBossResultInput) => void
|
|
|
|
|
onClose: () => void
|
|
|
|
|
isPending?: boolean
|
2026-02-08 12:03:11 +01:00
|
|
|
hardcoreMode?: boolean
|
2026-02-08 21:33:28 +01:00
|
|
|
starterName?: string | null
|
2026-02-08 11:16:13 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 20:39:41 +01:00
|
|
|
function matchVariant(labels: string[], starterName?: string | null): string | null {
|
2026-02-08 21:33:28 +01:00
|
|
|
if (!starterName || labels.length === 0) return null
|
|
|
|
|
const lower = starterName.toLowerCase()
|
|
|
|
|
const matches = labels.filter((l) => l.toLowerCase().includes(lower))
|
2026-02-16 20:39:41 +01:00
|
|
|
return matches.length === 1 ? (matches[0] ?? null) : null
|
2026-02-08 21:33:28 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 16:41:24 +01:00
|
|
|
export function BossDefeatModal({
|
|
|
|
|
boss,
|
|
|
|
|
onSubmit,
|
|
|
|
|
onClose,
|
|
|
|
|
isPending,
|
|
|
|
|
hardcoreMode,
|
|
|
|
|
starterName,
|
|
|
|
|
}: BossDefeatModalProps) {
|
2026-02-08 11:16:13 +01:00
|
|
|
const [result, setResult] = useState<'won' | 'lost'>('won')
|
|
|
|
|
const [attempts, setAttempts] = useState('1')
|
|
|
|
|
|
2026-02-08 21:20:30 +01:00
|
|
|
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
|
2026-02-14 16:41:24 +01:00
|
|
|
const autoMatch = useMemo(
|
|
|
|
|
() => matchVariant(variantLabels, starterName),
|
|
|
|
|
[variantLabels, starterName]
|
|
|
|
|
)
|
2026-02-08 21:33:28 +01:00
|
|
|
const showPills = hasVariants && autoMatch === null
|
2026-02-08 21:20:30 +01:00
|
|
|
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
2026-02-16 20:39:41 +01:00
|
|
|
autoMatch ?? (hasVariants ? (variantLabels[0] ?? null) : null)
|
2026-02-08 21:20:30 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const displayedPokemon = useMemo(() => {
|
|
|
|
|
if (!hasVariants) return boss.pokemon
|
|
|
|
|
return boss.pokemon.filter(
|
2026-02-16 20:39:41 +01:00
|
|
|
(bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null
|
2026-02-08 21:20:30 +01:00
|
|
|
)
|
|
|
|
|
}, [boss.pokemon, hasVariants, selectedVariant])
|
|
|
|
|
|
2026-02-08 11:16:13 +01:00
|
|
|
const handleSubmit = (e: FormEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
onSubmit({
|
|
|
|
|
bossBattleId: boss.id,
|
2026-02-08 12:03:11 +01:00
|
|
|
result: hardcoreMode ? 'won' : result,
|
|
|
|
|
attempts: hardcoreMode ? 1 : Number(attempts) || 1,
|
2026-02-08 11:16:13 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
|
|
|
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="relative bg-surface-1 rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
|
|
|
<div className="px-6 py-4 border-b border-border-default">
|
2026-02-08 11:16:13 +01:00
|
|
|
<h2 className="text-lg font-semibold">Battle: {boss.name}</h2>
|
2026-02-17 20:48:42 +01:00
|
|
|
<p className="text-sm text-text-tertiary">{boss.location}</p>
|
2026-02-08 11:16:13 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Boss team preview */}
|
|
|
|
|
{boss.pokemon.length > 0 && (
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="px-6 py-3 border-b border-border-default">
|
2026-02-08 21:33:28 +01:00
|
|
|
{showPills && (
|
2026-02-08 21:20:30 +01:00
|
|
|
<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
|
2026-02-17 21:08:53 +01:00
|
|
|
? 'bg-accent-600 text-white'
|
2026-02-17 20:48:42 +01:00
|
|
|
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
|
2026-02-08 21:20:30 +01:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-08 11:16:13 +01:00
|
|
|
<div className="flex flex-wrap gap-3">
|
2026-02-08 21:20:30 +01:00
|
|
|
{[...displayedPokemon]
|
2026-02-08 11:16:13 +01:00
|
|
|
.sort((a, b) => a.order - b.order)
|
|
|
|
|
.map((bp) => (
|
|
|
|
|
<div key={bp.id} className="flex flex-col items-center">
|
|
|
|
|
{bp.pokemon.spriteUrl ? (
|
2026-02-16 20:39:41 +01:00
|
|
|
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-10 h-10" />
|
2026-02-08 11:16:13 +01:00
|
|
|
) : (
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="w-10 h-10 bg-surface-3 rounded-full" />
|
2026-02-08 11:16:13 +01:00
|
|
|
)}
|
2026-02-17 20:48:42 +01:00
|
|
|
<span className="text-xs text-text-tertiary capitalize">{bp.pokemon.name}</span>
|
|
|
|
|
<span className="text-xs font-medium text-text-secondary">Lv.{bp.level}</span>
|
2026-02-16 21:17:32 +01:00
|
|
|
<ConditionBadge condition={bp.conditionLabel} size="xs" />
|
2026-02-08 11:16:13 +01:00
|
|
|
</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'
|
2026-02-17 20:48:42 +01:00
|
|
|
: 'border-border-default hover:bg-surface-2'
|
2026-02-08 11:16:13 +01:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Won
|
|
|
|
|
</button>
|
2026-02-08 12:03:11 +01:00
|
|
|
{!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'
|
2026-02-17 20:48:42 +01:00
|
|
|
: 'border-border-default hover:bg-surface-2'
|
2026-02-08 12:03:11 +01:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
Lost
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-02-08 11:16:13 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-08 12:03:11 +01:00
|
|
|
{!hardcoreMode && (
|
|
|
|
|
<div>
|
2026-02-16 20:39:41 +01:00
|
|
|
<label className="block text-sm font-medium mb-1">Attempts</label>
|
2026-02-08 12:03:11 +01:00
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
min={1}
|
|
|
|
|
value={attempts}
|
|
|
|
|
onChange={(e) => setAttempts(e.target.value)}
|
2026-02-17 20:48:42 +01:00
|
|
|
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
2026-02-08 12:03:11 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-08 11:16:13 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 20:48:42 +01:00
|
|
|
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
|
2026-02-08 11:16:13 +01:00
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onClose}
|
2026-02-17 20:48:42 +01:00
|
|
|
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
|
2026-02-08 11:16:13 +01:00
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={isPending}
|
2026-02-17 20:48:42 +01:00
|
|
|
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
|
2026-02-08 11:16:13 +01:00
|
|
|
>
|
|
|
|
|
{isPending ? 'Saving...' : 'Save Result'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|