Add pre-commit hooks for linting and formatting
All checks were successful
CI / backend-lint (push) Successful in 9s
CI / frontend-lint (push) Successful in 33s

Set up pre-commit framework with ruff (backend) and ESLint/Prettier/tsc
(frontend) hooks to catch issues locally before CI. Auto-format all
frontend files with Prettier to comply with the new check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 16:41:24 +01:00
parent b05a75f7f2
commit 2963f16aa4
67 changed files with 1905 additions and 792 deletions

View File

@@ -3,9 +3,17 @@ import { useParams, Link, useNavigate } from 'react-router-dom'
import { useRun, useUpdateRun } from '../hooks/useRuns'
import { useAdvanceLeg } from '../hooks/useGenlockes'
import { useGameRoutes } from '../hooks/useGames'
import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hooks/useEncounters'
import {
useCreateEncounter,
useUpdateEncounter,
useBulkRandomize,
} from '../hooks/useEncounters'
import { usePokemonFamilies } from '../hooks/usePokemon'
import { useGameBosses, useBossResults, useCreateBossResult } from '../hooks/useBosses'
import {
useGameBosses,
useBossResults,
useCreateBossResult,
} from '../hooks/useBosses'
import {
EggEncounterModal,
EncounterModal,
@@ -35,7 +43,10 @@ import type {
type TeamSortKey = 'route' | 'level' | 'species' | 'dex'
function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): EncounterDetail[] {
function sortEncounters(
encounters: EncounterDetail[],
key: TeamSortKey
): EncounterDetail[] {
return [...encounters].sort((a, b) => {
switch (key) {
case 'route':
@@ -48,7 +59,10 @@ function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): Encoun
return nameA.localeCompare(nameB)
}
case 'dex':
return (a.currentPokemon ?? a.pokemon).nationalDex - (b.currentPokemon ?? b.pokemon).nationalDex
return (
(a.currentPokemon ?? a.pokemon).nationalDex -
(b.currentPokemon ?? b.pokemon).nationalDex
)
default:
return 0
}
@@ -56,9 +70,9 @@ function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): Encoun
}
const statusStyles: Record<RunStatus, string> = {
active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
completed:
'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
active:
'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
completed: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300',
}
@@ -129,7 +143,7 @@ function organizeRoutes(routes: Route[]): RouteWithChildren[] {
*/
function getGroupEncounter(
group: RouteWithChildren,
encounterByRoute: Map<number, EncounterDetail>,
encounterByRoute: Map<number, EncounterDetail>
): EncounterDetail | null {
for (const child of group.children) {
const enc = encounterByRoute.get(child.id)
@@ -154,7 +168,7 @@ function effectiveZone(route: Route): number {
*/
function getZoneEncounters(
group: RouteWithChildren,
encounterByRoute: Map<number, EncounterDetail>,
encounterByRoute: Map<number, EncounterDetail>
): Map<number, EncounterDetail> {
const zoneMap = new Map<number, EncounterDetail>()
for (const child of group.children) {
@@ -172,14 +186,23 @@ function countDistinctZones(group: RouteWithChildren): number {
return zones.size
}
function matchVariant(labels: string[], starterName?: string | null): 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
}
function BossTeamPreview({ pokemon, starterName }: { pokemon: BossPokemon[]; starterName?: string | null }) {
function BossTeamPreview({
pokemon,
starterName,
}: {
pokemon: BossPokemon[]
starterName?: string | null
}) {
const variantLabels = useMemo(() => {
const labels = new Set<string>()
for (const bp of pokemon) {
@@ -189,16 +212,20 @@ function BossTeamPreview({ pokemon, starterName }: { pokemon: BossPokemon[]; sta
}, [pokemon])
const hasVariants = variantLabels.length > 0
const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
const autoMatch = useMemo(
() => matchVariant(variantLabels, starterName),
[variantLabels, starterName]
)
const showPills = hasVariants && autoMatch === null
const [selectedVariant, setSelectedVariant] = useState<string | null>(
autoMatch ?? (hasVariants ? variantLabels[0] : null),
autoMatch ?? (hasVariants ? variantLabels[0] : null)
)
const displayed = useMemo(() => {
if (!hasVariants) return pokemon
return pokemon.filter(
(bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null,
(bp) =>
bp.conditionLabel === selectedVariant || bp.conditionLabel === null
)
}, [pokemon, hasVariants, selectedVariant])
@@ -228,7 +255,11 @@ function BossTeamPreview({ pokemon, starterName }: { pokemon: BossPokemon[]; sta
.map((bp) => (
<div key={bp.id} className="flex items-center gap-1">
{bp.pokemon.spriteUrl ? (
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-20 h-20" />
<img
src={bp.pokemon.spriteUrl}
alt={bp.pokemon.name}
className="w-20 h-20"
/>
) : (
<div className="w-20 h-20 bg-gray-200 dark:bg-gray-700 rounded-full" />
)}
@@ -420,7 +451,7 @@ export function RunEncounters() {
const advanceLeg = useAdvanceLeg()
const [showTransferModal, setShowTransferModal] = useState(false)
const { data: routes, isLoading: loadingRoutes } = useGameRoutes(
run?.gameId ?? null,
run?.gameId ?? null
)
const createEncounter = useCreateEncounter(runIdNum)
const updateEncounter = useUpdateEncounter(runIdNum)
@@ -451,7 +482,9 @@ export function RunEncounters() {
try {
const saved = localStorage.getItem(storageKey)
if (saved) return new Set(JSON.parse(saved) as number[])
} catch { /* ignore */ }
} catch {
/* ignore */
}
return new Set<number>()
})
@@ -463,7 +496,7 @@ export function RunEncounters() {
return next
})
},
[storageKey],
[storageKey]
)
// Organize routes into hierarchical structure
@@ -475,25 +508,35 @@ export function RunEncounters() {
// Split encounters into normal (non-shiny) and shiny
const transferIdSet = useMemo(
() => new Set(run?.transferEncounterIds ?? []),
[run?.transferEncounterIds],
[run?.transferEncounterIds]
)
const { normalEncounters, shinyEncounters, transferEncounters } = useMemo(() => {
if (!run) return { normalEncounters: [], shinyEncounters: [], transferEncounters: [] }
const normal: EncounterDetail[] = []
const shiny: EncounterDetail[] = []
const transfer: EncounterDetail[] = []
for (const enc of run.encounters) {
if (transferIdSet.has(enc.id)) {
transfer.push(enc)
} else if (enc.isShiny) {
shiny.push(enc)
} else {
normal.push(enc)
const { normalEncounters, shinyEncounters, transferEncounters } =
useMemo(() => {
if (!run)
return {
normalEncounters: [],
shinyEncounters: [],
transferEncounters: [],
}
const normal: EncounterDetail[] = []
const shiny: EncounterDetail[] = []
const transfer: EncounterDetail[] = []
for (const enc of run.encounters) {
if (transferIdSet.has(enc.id)) {
transfer.push(enc)
} else if (enc.isShiny) {
shiny.push(enc)
} else {
normal.push(enc)
}
}
}
return { normalEncounters: normal, shinyEncounters: shiny, transferEncounters: transfer }
}, [run, transferIdSet])
return {
normalEncounters: normal,
shinyEncounters: shiny,
transferEncounters: transfer,
}
}, [run, transferIdSet])
// Map routeId → encounter for quick lookup (normal encounters only)
const encounterByRoute = useMemo(() => {
@@ -635,8 +678,7 @@ export function RunEncounters() {
if (organizedRoutes.length === 0 || expandedGroups.size > 0) return
const firstUnvisited = organizedRoutes.find(
(r) =>
r.children.length > 0 &&
getGroupEncounter(r, encounterByRoute) === null,
r.children.length > 0 && getGroupEncounter(r, encounterByRoute) === null
)
if (firstUnvisited) {
updateExpandedGroups(() => new Set([firstUnvisited.id]))
@@ -644,21 +686,25 @@ export function RunEncounters() {
}, [organizedRoutes, encounterByRoute]) // eslint-disable-line react-hooks/exhaustive-deps
const alive = useMemo(
() => sortEncounters(
[...normalEncounters, ...transferEncounters, ...shinyEncounters].filter(
(e) => e.status === 'caught' && e.faintLevel === null,
() =>
sortEncounters(
[...normalEncounters, ...transferEncounters, ...shinyEncounters].filter(
(e) => e.status === 'caught' && e.faintLevel === null
),
teamSort
),
teamSort,
),
[normalEncounters, transferEncounters, shinyEncounters, teamSort],
[normalEncounters, transferEncounters, shinyEncounters, teamSort]
)
const dead = useMemo(
() => sortEncounters(
normalEncounters.filter((e) => e.status === 'caught' && e.faintLevel !== null),
teamSort,
),
[normalEncounters, teamSort],
() =>
sortEncounters(
normalEncounters.filter(
(e) => e.status === 'caught' && e.faintLevel !== null
),
teamSort
),
[normalEncounters, teamSort]
)
// Resolve HoF team encounters from IDs
@@ -810,7 +856,10 @@ export function RunEncounters() {
{run.name}
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
{run.game.name} &middot; {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} &middot; Started{' '}
{run.game.name} &middot;{' '}
{run.game.region.charAt(0).toUpperCase() +
run.game.region.slice(1)}{' '}
&middot; Started{' '}
{new Date(run.startedAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
@@ -819,7 +868,8 @@ export function RunEncounters() {
</p>
{run.genlocke && (
<p className="text-sm text-purple-600 dark:text-purple-400 mt-1 font-medium">
Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} &mdash; {run.genlocke.genlockeName}
Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} &mdash;{' '}
{run.genlocke.genlockeName}
</p>
)}
</div>
@@ -868,7 +918,9 @@ export function RunEncounters() {
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}</span>
<span className="text-2xl">
{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}
</span>
<div>
<p
className={`font-semibold ${
@@ -907,33 +959,40 @@ export function RunEncounters() {
</p>
</div>
</div>
{run.status === 'completed' && run.genlocke && !run.genlocke.isFinalLeg && (
<button
onClick={() => {
if (hofTeam && hofTeam.length > 0) {
setShowTransferModal(true)
} else {
advanceLeg.mutate(
{ genlockeId: run.genlocke!.genlockeId, legOrder: run.genlocke!.legOrder },
{
onSuccess: (genlocke) => {
const nextLeg = genlocke.legs.find(
(l) => l.legOrder === run.genlocke!.legOrder + 1,
)
if (nextLeg?.runId) {
navigate(`/runs/${nextLeg.runId}`)
}
{run.status === 'completed' &&
run.genlocke &&
!run.genlocke.isFinalLeg && (
<button
onClick={() => {
if (hofTeam && hofTeam.length > 0) {
setShowTransferModal(true)
} else {
advanceLeg.mutate(
{
genlockeId: run.genlocke!.genlockeId,
legOrder: run.genlocke!.legOrder,
},
},
)
}
}}
disabled={advanceLeg.isPending}
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{advanceLeg.isPending ? 'Advancing...' : 'Advance to Next Leg'}
</button>
)}
{
onSuccess: (genlocke) => {
const nextLeg = genlocke.legs.find(
(l) => l.legOrder === run.genlocke!.legOrder + 1
)
if (nextLeg?.runId) {
navigate(`/runs/${nextLeg.runId}`)
}
},
}
)
}
}}
disabled={advanceLeg.isPending}
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{advanceLeg.isPending
? 'Advancing...'
: 'Advance to Next Leg'}
</button>
)}
</div>
{/* HoF Team Display */}
{run.status === 'completed' && (
@@ -957,7 +1016,11 @@ export function RunEncounters() {
return (
<div key={enc.id} className="flex flex-col items-center">
{dp.spriteUrl ? (
<img src={dp.spriteUrl} alt={dp.name} className="w-12 h-12" />
<img
src={dp.spriteUrl}
alt={dp.name}
className="w-12 h-12"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold">
{dp.name[0].toUpperCase()}
@@ -1040,11 +1103,13 @@ export function RunEncounters() {
className="w-6 h-6"
/>
) : (
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
earned
? 'border-yellow-500 bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300'
: 'border-gray-300 dark:border-gray-600 text-gray-400'
}`}>
<div
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
earned
? 'border-yellow-500 bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300'
: 'border-gray-300 dark:border-gray-600 text-gray-400'
}`}
>
{boss.order}
</div>
)}
@@ -1077,7 +1142,8 @@ export function RunEncounters() {
{isActive ? 'Team' : 'Final Team'}
</h2>
<span className="text-xs text-gray-400 dark:text-gray-500">
{alive.length} alive{dead.length > 0 ? `, ${dead.length} dead` : ''}
{alive.length} alive
{dead.length > 0 ? `, ${dead.length} dead` : ''}
</span>
<svg
className={`w-4 h-4 text-gray-400 transition-transform ${showTeam ? 'rotate-180' : ''}`}
@@ -1114,7 +1180,11 @@ export function RunEncounters() {
<PokemonCard
key={enc.id}
encounter={enc}
onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
onClick={
isActive
? () => setSelectedTeamEncounter(enc)
: undefined
}
/>
))}
</div>
@@ -1130,7 +1200,11 @@ export function RunEncounters() {
key={enc.id}
encounter={enc}
showFaintLevel
onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
onClick={
isActive
? () => setSelectedTeamEncounter(enc)
: undefined
}
/>
))}
</div>
@@ -1146,7 +1220,9 @@ export function RunEncounters() {
<div className="mb-6">
<ShinyBox
encounters={shinyEncounters}
onEncounterClick={isActive ? (enc) => setSelectedTeamEncounter(enc) : undefined}
onEncounterClick={
isActive ? (enc) => setSelectedTeamEncounter(enc) : undefined
}
/>
</div>
)}
@@ -1162,7 +1238,9 @@ export function RunEncounters() {
<PokemonCard
key={enc.id}
encounter={enc}
onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
onClick={
isActive ? () => setSelectedTeamEncounter(enc) : undefined
}
/>
))}
</div>
@@ -1182,7 +1260,11 @@ export function RunEncounters() {
disabled={bulkRandomize.isPending}
onClick={() => {
const remaining = totalLocations - completedCount
if (window.confirm(`Randomize encounters for all ${remaining} remaining locations?`)) {
if (
window.confirm(
`Randomize encounters for all ${remaining} remaining locations?`
)
) {
bulkRandomize.mutate()
}
}}
@@ -1242,9 +1324,10 @@ export function RunEncounters() {
)}
{filteredRoutes.map((route) => {
// Collect all route IDs to check for boss cards after
const routeIds: number[] = route.children.length > 0
? [route.id, ...route.children.map((c) => c.id)]
: [route.id]
const routeIds: number[] =
route.children.length > 0
? [route.id, ...route.children.map((c) => c.id)]
: [route.id]
// Find boss battles positioned after this route (or any of its children)
const bossesHere: BossBattle[] = []
@@ -1253,68 +1336,77 @@ export function RunEncounters() {
if (b) bossesHere.push(...b)
}
const routeElement = route.children.length > 0 ? (
<RouteGroup
key={route.id}
group={route}
encounterByRoute={encounterByRoute}
isExpanded={expandedGroups.has(route.id)}
onToggleExpand={() => toggleGroup(route.id)}
onRouteClick={handleRouteClick}
filter={filter}
pinwheelClause={pinwheelClause}
/>
) : (() => {
const encounter = encounterByRoute.get(route.id)
const rs = getRouteStatus(encounter)
const si = statusIndicator[rs]
return (
<button
const routeElement =
route.children.length > 0 ? (
<RouteGroup
key={route.id}
type="button"
onClick={() => handleRouteClick(route)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
>
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`}
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{route.name}
</div>
{encounter ? (
<div className="flex items-center gap-2 mt-0.5">
{encounter.pokemon.spriteUrl && (
<img
src={encounter.pokemon.spriteUrl}
alt={encounter.pokemon.name}
className="w-10 h-10"
/>
group={route}
encounterByRoute={encounterByRoute}
isExpanded={expandedGroups.has(route.id)}
onToggleExpand={() => toggleGroup(route.id)}
onRouteClick={handleRouteClick}
filter={filter}
pinwheelClause={pinwheelClause}
/>
) : (
(() => {
const encounter = encounterByRoute.get(route.id)
const rs = getRouteStatus(encounter)
const si = statusIndicator[rs]
return (
<button
key={route.id}
type="button"
onClick={() => handleRouteClick(route)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
>
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`}
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{route.name}
</div>
{encounter ? (
<div className="flex items-center gap-2 mt-0.5">
{encounter.pokemon.spriteUrl && (
<img
src={encounter.pokemon.spriteUrl}
alt={encounter.pokemon.name}
className="w-10 h-10"
/>
)}
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{encounter.nickname ?? encounter.pokemon.name}
{encounter.status === 'caught' &&
encounter.faintLevel !== null &&
(encounter.deathCause
? `${encounter.deathCause}`
: ' (dead)')}
</span>
</div>
) : (
route.encounterMethods.length > 0 && (
<div className="flex flex-wrap gap-1 mt-0.5">
{route.encounterMethods.map((m) => (
<EncounterMethodBadge
key={m}
method={m}
size="xs"
/>
))}
</div>
)
)}
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
{encounter.nickname ?? encounter.pokemon.name}
{encounter.status === 'caught' &&
encounter.faintLevel !== null &&
(encounter.deathCause
? `${encounter.deathCause}`
: ' (dead)')}
</span>
</div>
) : route.encounterMethods.length > 0 && (
<div className="flex flex-wrap gap-1 mt-0.5">
{route.encounterMethods.map((m) => (
<EncounterMethodBadge key={m} method={m} size="xs" />
))}
</div>
)}
</div>
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
{si.label}
</span>
</button>
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
{si.label}
</span>
</button>
)
})()
)
})()
return (
<div key={route.id}>
@@ -1358,67 +1450,83 @@ export function RunEncounters() {
<div key={`boss-${boss.id}`}>
<div
className={`my-2 rounded-lg border-2 ${bossTypeColors[boss.bossType] ?? bossTypeColors.other} ${
isDefeated ? 'bg-green-50/50 dark:bg-green-900/10' : 'bg-white dark:bg-gray-800'
isDefeated
? 'bg-green-50/50 dark:bg-green-900/10'
: 'bg-white dark:bg-gray-800'
} px-4 py-3`}
>
<div
className="flex items-start justify-between cursor-pointer select-none"
onClick={toggleBoss}
>
<div className="flex items-center gap-3">
<svg
className={`w-4 h-4 text-gray-400 transition-transform ${isBossExpanded ? 'rotate-90' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
{boss.spriteUrl && (
<img src={boss.spriteUrl} alt={boss.name} className="h-10 w-auto" />
)}
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{boss.name}
</span>
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
{bossTypeLabel[boss.bossType] ?? boss.bossType}
</span>
{boss.specialtyType && (
<TypeBadge type={boss.specialtyType} />
)}
<div
className="flex items-start justify-between cursor-pointer select-none"
onClick={toggleBoss}
>
<div className="flex items-center gap-3">
<svg
className={`w-4 h-4 text-gray-400 transition-transform ${isBossExpanded ? 'rotate-90' : ''}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 5l7 7-7 7"
/>
</svg>
{boss.spriteUrl && (
<img
src={boss.spriteUrl}
alt={boss.name}
className="h-10 w-auto"
/>
)}
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
{boss.name}
</span>
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
{bossTypeLabel[boss.bossType] ?? boss.bossType}
</span>
{boss.specialtyType && (
<TypeBadge type={boss.specialtyType} />
)}
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{boss.location} &middot; Level Cap:{' '}
{boss.levelCap}
</p>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
{boss.location} &middot; Level Cap: {boss.levelCap}
</p>
</div>
<div onClick={(e) => e.stopPropagation()}>
{isDefeated ? (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300">
Defeated &#10003;
</span>
) : isActive ? (
<button
onClick={() => setSelectedBoss(boss)}
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
>
Battle
</button>
) : null}
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
{isDefeated ? (
<span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300">
Defeated &#10003;
</span>
) : isActive ? (
<button
onClick={() => setSelectedBoss(boss)}
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
>
Battle
</button>
) : null}
</div>
</div>
{/* Boss pokemon team */}
{isBossExpanded && boss.pokemon.length > 0 && (
<BossTeamPreview pokemon={boss.pokemon} starterName={starterName} />
)}
{/* Boss pokemon team */}
{isBossExpanded && boss.pokemon.length > 0 && (
<BossTeamPreview
pokemon={boss.pokemon}
starterName={starterName}
/>
)}
</div>
{sectionAfter && (
<div className="flex items-center gap-3 my-4">
<div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
<span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">{sectionAfter}</span>
<span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{sectionAfter}
</span>
<div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
</div>
)}
@@ -1519,7 +1627,7 @@ export function RunEncounters() {
setShowHofModal(true)
}
},
},
}
)
}}
onClose={() => setShowEndRun(false)}
@@ -1535,7 +1643,7 @@ export function RunEncounters() {
onSubmit={(encounterIds) => {
updateRun.mutate(
{ hofEncounterIds: encounterIds },
{ onSuccess: () => setShowHofModal(false) },
{ onSuccess: () => setShowHofModal(false) }
)
}}
onSkip={() => setShowHofModal(false)}
@@ -1558,13 +1666,13 @@ export function RunEncounters() {
onSuccess: (genlocke) => {
setShowTransferModal(false)
const nextLeg = genlocke.legs.find(
(l) => l.legOrder === run!.genlocke!.legOrder + 1,
(l) => l.legOrder === run!.genlocke!.legOrder + 1
)
if (nextLeg?.runId) {
navigate(`/runs/${nextLeg.runId}`)
}
},
},
}
)
}}
onSkip={() => {
@@ -1577,13 +1685,13 @@ export function RunEncounters() {
onSuccess: (genlocke) => {
setShowTransferModal(false)
const nextLeg = genlocke.legs.find(
(l) => l.legOrder === run!.genlocke!.legOrder + 1,
(l) => l.legOrder === run!.genlocke!.legOrder + 1
)
if (nextLeg?.runId) {
navigate(`/runs/${nextLeg.runId}`)
}
},
},
}
)
}}
isPending={advanceLeg.isPending}