Add pre-commit hooks for linting and formatting
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:
@@ -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} · {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '}
|
||||
{run.game.name} ·{' '}
|
||||
{run.game.region.charAt(0).toUpperCase() +
|
||||
run.game.region.slice(1)}{' '}
|
||||
· 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} — {run.genlocke.genlockeName}
|
||||
Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} —{' '}
|
||||
{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} · Level Cap:{' '}
|
||||
{boss.levelCap}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{boss.location} · 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 ✓
|
||||
</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 ✓
|
||||
</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}
|
||||
|
||||
Reference in New Issue
Block a user