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>
133 lines
4.7 KiB
TypeScript
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>
|
|
)
|
|
}
|