Align repo config with global development standards
- Add missing tsconfig strictness flags (noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noPropertyAccessFromIndexSignature) and fix all resulting type errors - Replace ESLint/Prettier with oxlint 1.48.0 and oxfmt 0.33.0 - Pin all frontend and backend dependencies to exact versions - Pin GitHub Actions to SHA hashes with persist-credentials: false - Fix CI Python version mismatch (3.12 -> 3.14) and ruff target-version - Add vitest 4.0.18 with jsdom environment for frontend testing - Add ty 0.0.17 for Python type checking (non-blocking in CI) - Add actionlint and zizmor CI job for workflow linting and security audit - Add Dependabot config for npm, pip, and github-actions - Update CLAUDE.md and pre-commit hooks to reflect new tooling - Ignore Claude Code sandbox artifacts in gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,17 +3,9 @@ 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,
|
||||
@@ -43,10 +35,7 @@ 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':
|
||||
@@ -60,8 +49,7 @@ function sortEncounters(
|
||||
}
|
||||
case 'dex':
|
||||
return (
|
||||
(a.currentPokemon ?? a.pokemon).nationalDex -
|
||||
(b.currentPokemon ?? b.pokemon).nationalDex
|
||||
(a.currentPokemon ?? a.pokemon).nationalDex - (b.currentPokemon ?? b.pokemon).nationalDex
|
||||
)
|
||||
default:
|
||||
return 0
|
||||
@@ -70,8 +58,7 @@ function sortEncounters(
|
||||
}
|
||||
|
||||
const statusStyles: Record<RunStatus, string> = {
|
||||
active:
|
||||
'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-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',
|
||||
}
|
||||
@@ -91,10 +78,7 @@ function getRouteStatus(encounter?: EncounterDetail): RouteStatus {
|
||||
return encounter.status
|
||||
}
|
||||
|
||||
const statusIndicator: Record<
|
||||
RouteStatus,
|
||||
{ dot: string; label: string; bg: string }
|
||||
> = {
|
||||
const statusIndicator: Record<RouteStatus, { dot: string; label: string; bg: string }> = {
|
||||
caught: {
|
||||
dot: 'bg-green-500',
|
||||
label: 'Caught',
|
||||
@@ -186,14 +170,11 @@ 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
|
||||
return matches.length === 1 ? (matches[0] ?? null) : null
|
||||
}
|
||||
|
||||
function BossTeamPreview({
|
||||
@@ -218,14 +199,13 @@ function BossTeamPreview({
|
||||
)
|
||||
const showPills = hasVariants && autoMatch === null
|
||||
const [selectedVariant, setSelectedVariant] = useState<string | null>(
|
||||
autoMatch ?? (hasVariants ? variantLabels[0] : null)
|
||||
autoMatch ?? (hasVariants ? (variantLabels[0] ?? null) : 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])
|
||||
|
||||
@@ -255,17 +235,11 @@ function BossTeamPreview({
|
||||
.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" />
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Lvl {bp.level}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Lvl {bp.level}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -294,9 +268,7 @@ function RouteGroup({
|
||||
}: RouteGroupProps) {
|
||||
const groupEncounter = getGroupEncounter(group, encounterByRoute)
|
||||
const usePinwheel = pinwheelClause && groupHasZones(group)
|
||||
const zoneEncounters = usePinwheel
|
||||
? getZoneEncounters(group, encounterByRoute)
|
||||
: null
|
||||
const zoneEncounters = usePinwheel ? getZoneEncounters(group, encounterByRoute) : null
|
||||
|
||||
// For pinwheel groups, determine status from all zone statuses
|
||||
let groupStatus: RouteStatus
|
||||
@@ -354,28 +326,19 @@ function RouteGroup({
|
||||
{groupEncounter.nickname ?? groupEncounter.pokemon.name}
|
||||
{groupEncounter.status === 'caught' &&
|
||||
groupEncounter.faintLevel !== null &&
|
||||
(groupEncounter.deathCause
|
||||
? ` — ${groupEncounter.deathCause}`
|
||||
: ' (dead)')}
|
||||
(groupEncounter.deathCause ? ` — ${groupEncounter.deathCause}` : ' (dead)')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
|
||||
{si.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">{si.label}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -409,13 +372,9 @@ function RouteGroup({
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
||||
} ${childSi.bg}`}
|
||||
>
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full shrink-0 ${childSi.dot}`}
|
||||
/>
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${childSi.dot}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{child.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">{child.name}</div>
|
||||
{!childEncounter && child.encounterMethods.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||
{child.encounterMethods.map((m) => (
|
||||
@@ -425,14 +384,10 @@ function RouteGroup({
|
||||
)}
|
||||
</div>
|
||||
{childEncounter && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{childSi.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{childSi.label}</span>
|
||||
)}
|
||||
{isDisabled && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 italic">
|
||||
(locked)
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 italic">(locked)</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
@@ -450,9 +405,7 @@ export function RunEncounters() {
|
||||
const { data: run, isLoading, error } = useRun(runIdNum)
|
||||
const advanceLeg = useAdvanceLeg()
|
||||
const [showTransferModal, setShowTransferModal] = useState(false)
|
||||
const { data: routes, isLoading: loadingRoutes } = useGameRoutes(
|
||||
run?.gameId ?? null
|
||||
)
|
||||
const { data: routes, isLoading: loadingRoutes } = useGameRoutes(run?.gameId ?? null)
|
||||
const createEncounter = useCreateEncounter(runIdNum)
|
||||
const updateEncounter = useUpdateEncounter(runIdNum)
|
||||
const bulkRandomize = useBulkRandomize(runIdNum)
|
||||
@@ -464,10 +417,8 @@ export function RunEncounters() {
|
||||
|
||||
const [selectedRoute, setSelectedRoute] = useState<Route | null>(null)
|
||||
const [selectedBoss, setSelectedBoss] = useState<BossBattle | null>(null)
|
||||
const [editingEncounter, setEditingEncounter] =
|
||||
useState<EncounterDetail | null>(null)
|
||||
const [selectedTeamEncounter, setSelectedTeamEncounter] =
|
||||
useState<EncounterDetail | null>(null)
|
||||
const [editingEncounter, setEditingEncounter] = useState<EncounterDetail | null>(null)
|
||||
const [selectedTeamEncounter, setSelectedTeamEncounter] = useState<EncounterDetail | null>(null)
|
||||
const [showEndRun, setShowEndRun] = useState(false)
|
||||
const [showHofModal, setShowHofModal] = useState(false)
|
||||
const [showShinyModal, setShowShinyModal] = useState(false)
|
||||
@@ -511,32 +462,31 @@ export function RunEncounters() {
|
||||
[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: normal,
|
||||
shinyEncounters: shiny,
|
||||
transferEncounters: transfer,
|
||||
normalEncounters: [],
|
||||
shinyEncounters: [],
|
||||
transferEncounters: [],
|
||||
}
|
||||
}, [run, transferIdSet])
|
||||
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])
|
||||
|
||||
// Map routeId → encounter for quick lookup (normal encounters only)
|
||||
const encounterByRoute = useMemo(() => {
|
||||
@@ -638,9 +588,7 @@ export function RunEncounters() {
|
||||
const currentLevelCap = useMemo(() => {
|
||||
if (!nextBoss) {
|
||||
// All defeated — no cap (or use last boss's level)
|
||||
return sortedBosses.length > 0
|
||||
? sortedBosses[sortedBosses.length - 1].levelCap
|
||||
: null
|
||||
return sortedBosses.length > 0 ? sortedBosses[sortedBosses.length - 1]!.levelCap : null
|
||||
}
|
||||
return nextBoss.levelCap
|
||||
}, [nextBoss, sortedBosses])
|
||||
@@ -650,8 +598,8 @@ export function RunEncounters() {
|
||||
const sectionDividerAfterBoss = useMemo(() => {
|
||||
const map = new Map<number, string>()
|
||||
for (let i = 0; i < sortedBosses.length - 1; i++) {
|
||||
const current = sortedBosses[i]
|
||||
const next = sortedBosses[i + 1]
|
||||
const current = sortedBosses[i]!
|
||||
const next = sortedBosses[i + 1]!
|
||||
if (next.section != null && current.section !== next.section) {
|
||||
map.set(current.id, next.section)
|
||||
}
|
||||
@@ -677,8 +625,7 @@ export function RunEncounters() {
|
||||
useEffect(() => {
|
||||
if (organizedRoutes.length === 0 || expandedGroups.size > 0) return
|
||||
const firstUnvisited = organizedRoutes.find(
|
||||
(r) =>
|
||||
r.children.length > 0 && getGroupEncounter(r, encounterByRoute) === null
|
||||
(r) => r.children.length > 0 && getGroupEncounter(r, encounterByRoute) === null
|
||||
)
|
||||
if (firstUnvisited) {
|
||||
updateExpandedGroups(() => new Set([firstUnvisited.id]))
|
||||
@@ -699,9 +646,7 @@ export function RunEncounters() {
|
||||
const dead = useMemo(
|
||||
() =>
|
||||
sortEncounters(
|
||||
normalEncounters.filter(
|
||||
(e) => e.status === 'caught' && e.faintLevel !== null
|
||||
),
|
||||
normalEncounters.filter((e) => e.status === 'caught' && e.faintLevel !== null),
|
||||
teamSort
|
||||
),
|
||||
[normalEncounters, teamSort]
|
||||
@@ -728,10 +673,7 @@ export function RunEncounters() {
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
Failed to load run.
|
||||
</div>
|
||||
<Link
|
||||
to="/runs"
|
||||
className="inline-block mt-4 text-blue-600 hover:underline"
|
||||
>
|
||||
<Link to="/runs" className="inline-block mt-4 text-blue-600 hover:underline">
|
||||
Back to runs
|
||||
</Link>
|
||||
</div>
|
||||
@@ -803,10 +745,10 @@ export function RunEncounters() {
|
||||
const handleUpdate = (data: {
|
||||
id: number
|
||||
data: {
|
||||
nickname?: string
|
||||
status?: EncounterStatus
|
||||
faintLevel?: number
|
||||
deathCause?: string
|
||||
nickname?: string | undefined
|
||||
status?: EncounterStatus | undefined
|
||||
faintLevel?: number | undefined
|
||||
deathCause?: string | undefined
|
||||
}
|
||||
}) => {
|
||||
updateEncounter.mutate(data, {
|
||||
@@ -852,14 +794,10 @@ export function RunEncounters() {
|
||||
</Link>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{run.name}
|
||||
</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{run.name}</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
{run.game.name} ·{' '}
|
||||
{run.game.region.charAt(0).toUpperCase() +
|
||||
run.game.region.slice(1)}{' '}
|
||||
· Started{' '}
|
||||
{run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '}
|
||||
{new Date(run.startedAt).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
@@ -959,40 +897,36 @@ 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,
|
||||
{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}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
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>
|
||||
)}
|
||||
}
|
||||
)
|
||||
}
|
||||
}}
|
||||
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' && (
|
||||
@@ -1016,14 +950,10 @@ 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()}
|
||||
{dp.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-[10px] text-blue-600 dark:text-blue-400 capitalize mt-0.5">
|
||||
@@ -1045,19 +975,10 @@ export function RunEncounters() {
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<StatCard
|
||||
label="Encounters"
|
||||
value={normalEncounters.length}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard label="Encounters" value={normalEncounters.length} color="blue" />
|
||||
<StatCard label="Alive" value={alive.length} color="green" />
|
||||
<StatCard label="Deaths" value={dead.length} color="red" />
|
||||
<StatCard
|
||||
label="Routes"
|
||||
value={completedCount}
|
||||
total={totalLocations}
|
||||
color="purple"
|
||||
/>
|
||||
<StatCard label="Routes" value={completedCount} total={totalLocations} color="purple" />
|
||||
</div>
|
||||
|
||||
{/* Level Cap Bar */}
|
||||
@@ -1123,9 +1044,7 @@ export function RunEncounters() {
|
||||
|
||||
{/* Rules */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Active Rules
|
||||
</h2>
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Rules</h2>
|
||||
<RuleBadges rules={run.rules} />
|
||||
</div>
|
||||
|
||||
@@ -1180,11 +1099,7 @@ export function RunEncounters() {
|
||||
<PokemonCard
|
||||
key={enc.id}
|
||||
encounter={enc}
|
||||
onClick={
|
||||
isActive
|
||||
? () => setSelectedTeamEncounter(enc)
|
||||
: undefined
|
||||
}
|
||||
onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1200,11 +1115,7 @@ export function RunEncounters() {
|
||||
key={enc.id}
|
||||
encounter={enc}
|
||||
showFaintLevel
|
||||
onClick={
|
||||
isActive
|
||||
? () => setSelectedTeamEncounter(enc)
|
||||
: undefined
|
||||
}
|
||||
onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1220,9 +1131,7 @@ export function RunEncounters() {
|
||||
<div className="mb-6">
|
||||
<ShinyBox
|
||||
encounters={shinyEncounters}
|
||||
onEncounterClick={
|
||||
isActive ? (enc) => setSelectedTeamEncounter(enc) : undefined
|
||||
}
|
||||
onEncounterClick={isActive ? (enc) => setSelectedTeamEncounter(enc) : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1238,9 +1147,7 @@ export function RunEncounters() {
|
||||
<PokemonCard
|
||||
key={enc.id}
|
||||
encounter={enc}
|
||||
onClick={
|
||||
isActive ? () => setSelectedTeamEncounter(enc) : undefined
|
||||
}
|
||||
onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1251,9 +1158,7 @@ export function RunEncounters() {
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Encounters
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Encounters</h2>
|
||||
{isActive && completedCount < totalLocations && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1261,9 +1166,7 @@ export function RunEncounters() {
|
||||
onClick={() => {
|
||||
const remaining = totalLocations - completedCount
|
||||
if (
|
||||
window.confirm(
|
||||
`Randomize encounters for all ${remaining} remaining locations?`
|
||||
)
|
||||
window.confirm(`Randomize encounters for all ${remaining} remaining locations?`)
|
||||
) {
|
||||
bulkRandomize.mutate()
|
||||
}
|
||||
@@ -1325,9 +1228,7 @@ 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]
|
||||
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[] = []
|
||||
@@ -1361,9 +1262,7 @@ export function RunEncounters() {
|
||||
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}`}
|
||||
/>
|
||||
<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}
|
||||
@@ -1381,20 +1280,14 @@ export function RunEncounters() {
|
||||
{encounter.nickname ?? encounter.pokemon.name}
|
||||
{encounter.status === 'caught' &&
|
||||
encounter.faintLevel !== null &&
|
||||
(encounter.deathCause
|
||||
? ` — ${encounter.deathCause}`
|
||||
: ' (dead)')}
|
||||
(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"
|
||||
/>
|
||||
<EncounterMethodBadge key={m} method={m} size="xs" />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -1449,7 +1342,7 @@ export function RunEncounters() {
|
||||
return (
|
||||
<div key={`boss-${boss.id}`}>
|
||||
<div
|
||||
className={`my-2 rounded-lg border-2 ${bossTypeColors[boss.bossType] ?? bossTypeColors.other} ${
|
||||
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'
|
||||
@@ -1467,18 +1360,10 @@ export function RunEncounters() {
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
<img src={boss.spriteUrl} alt={boss.name} className="h-10 w-auto" />
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -1488,13 +1373,10 @@ export function RunEncounters() {
|
||||
<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} />
|
||||
)}
|
||||
{boss.specialtyType && <TypeBadge type={boss.specialtyType} />}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{boss.location} · Level Cap:{' '}
|
||||
{boss.levelCap}
|
||||
{boss.location} · Level Cap: {boss.levelCap}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1515,10 +1397,7 @@ export function RunEncounters() {
|
||||
</div>
|
||||
{/* Boss pokemon team */}
|
||||
{isBossExpanded && boss.pokemon.length > 0 && (
|
||||
<BossTeamPreview
|
||||
pokemon={boss.pokemon}
|
||||
starterName={starterName}
|
||||
/>
|
||||
<BossTeamPreview pokemon={boss.pokemon} starterName={starterName} />
|
||||
)}
|
||||
</div>
|
||||
{sectionAfter && (
|
||||
|
||||
Reference in New Issue
Block a user