Files
nuzlocke-tracker/frontend/src/components/BossDefeatModal.tsx
Julian Tabel 1cd1389408
Some checks failed
CI / backend-tests (push) Successful in 28s
CI / frontend-tests (push) Failing after 28s
Replace playstyle rules with free-text custom rules markdown field
Remove hardcoreMode, setModeOnly, and bossTeamMatch toggles which had
no mechanical impact on the tracker. Replace them with a customRules
markdown field so users can document their own rules (especially useful
for genlockes). Add react-markdown + remark-gfm for rendering and
@tailwindcss/typography for prose styling. The custom rules display is
collapsible and hidden by default.

Also simplifies the BossDefeatModal by removing the Lost result and
attempts counter, and always shows boss team size in the level cap bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 15:09:02 +01:00

133 lines
4.7 KiB
TypeScript

import { type FormEvent, useMemo, useState } from 'react'
import type { BossBattle, CreateBossResultInput } from '../types/game'
import { ConditionBadge } from './ConditionBadge'
interface BossDefeatModalProps {
boss: BossBattle
onSubmit: (data: CreateBossResultInput) => void
onClose: () => void
isPending?: 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) : null
}
export function BossDefeatModal({
boss,
onSubmit,
onClose,
isPending,
starterName,
}: BossDefeatModalProps) {
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) : 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: 'won',
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-surface-1 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="px-6 py-4 border-b border-border-default">
<h2 className="text-lg font-semibold">Battle: {boss.name}</h2>
<p className="text-sm text-text-tertiary">{boss.location}</p>
</div>
{/* Boss team preview */}
{boss.pokemon.length > 0 && (
<div className="px-6 py-3 border-b border-border-default">
{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-accent-600 text-white'
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
}`}
>
{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-surface-3 rounded-full" />
)}
<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>
<ConditionBadge condition={bp.conditionLabel} size="xs" />
</div>
))}
</div>
</div>
)}
<form onSubmit={handleSubmit}>
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
>
Cancel
</button>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
>
{isPending ? 'Saving...' : 'Save Result'}
</button>
</div>
</form>
</div>
</div>
)
}