Files
nuzlocke-tracker/frontend/src/components/admin/BossTeamEditor.tsx
Julian Tabel 190b08eb26 Add boss battles, level caps, and badge tracking
Introduces full boss battle system: data models (BossBattle, BossPokemon,
BossResult), API endpoints for CRUD and per-run defeat tracking, and frontend
UI including a sticky level cap bar with badge display on the run page,
interleaved boss battle cards in the encounter list, and an admin panel
section for managing boss battles and their pokemon teams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 11:16:13 +01:00

130 lines
4.6 KiB
TypeScript

import { type FormEvent, useState } from 'react'
import { PokemonSelector } from './PokemonSelector'
import type { BossBattle } from '../../types/game'
import type { BossPokemonInput } from '../../types/admin'
interface BossTeamEditorProps {
boss: BossBattle
onSave: (team: BossPokemonInput[]) => void
onClose: () => void
isSaving?: boolean
}
export function BossTeamEditor({ boss, onSave, onClose, isSaving }: BossTeamEditorProps) {
const [team, setTeam] = useState<Array<{ pokemonId: number | null; pokemonName: string; level: string; order: number }>>(
boss.pokemon.length > 0
? boss.pokemon
.sort((a, b) => a.order - b.order)
.map((bp) => ({
pokemonId: bp.pokemonId,
pokemonName: bp.pokemon.name,
level: String(bp.level),
order: bp.order,
}))
: [{ pokemonId: null, pokemonName: '', level: '', order: 1 }],
)
const addSlot = () => {
setTeam((prev) => [
...prev,
{ pokemonId: null, pokemonName: '', level: '', order: prev.length + 1 },
])
}
const removeSlot = (index: number) => {
setTeam((prev) => prev.filter((_, i) => i !== index).map((item, i) => ({ ...item, order: i + 1 })))
}
const updateSlot = (index: number, field: string, value: number | string | null) => {
setTeam((prev) =>
prev.map((item, i) => (i === index ? { ...item, [field]: value } : item)),
)
}
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
const validTeam: BossPokemonInput[] = team
.filter((t) => t.pokemonId != null && t.level)
.map((t, i) => ({
pokemonId: t.pokemonId!,
level: Number(t.level),
order: i + 1,
}))
onSave(validTeam)
}
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-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold">{boss.name}'s Team</h2>
</div>
<form onSubmit={handleSubmit}>
<div className="px-6 py-4 space-y-3">
{team.map((slot, index) => (
<div key={index} className="flex items-end gap-2">
<div className="flex-1">
<PokemonSelector
label={`Pokemon ${index + 1}`}
selectedId={slot.pokemonId}
initialName={slot.pokemonName}
onChange={(id) => updateSlot(index, 'pokemonId', id)}
/>
</div>
<div className="w-20">
<label className="block text-sm font-medium mb-1">Level</label>
<input
type="number"
min={1}
max={100}
value={slot.level}
onChange={(e) => updateSlot(index, 'level', e.target.value)}
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
/>
</div>
<button
type="button"
onClick={() => removeSlot(index)}
className="px-2 py-2 text-red-500 hover:text-red-700 text-sm"
title="Remove"
>
&#10005;
</button>
</div>
))}
{team.length < 6 && (
<button
type="button"
onClick={addSlot}
className="w-full py-2 text-sm text-blue-600 dark:text-blue-400 border border-dashed border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700"
>
+ Add Pokemon
</button>
)}
</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={isSaving}
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isSaving ? 'Saving...' : 'Save Team'}
</button>
</div>
</form>
</div>
</div>
)
}