Fix team sort: add to RunEncounters and fix hook ordering
Add sort dropdown to RunEncounters (the encounters page with the expandable team section) and move all useMemo hooks before early returns in both RunDashboard and RunEncounters to fix React hook ordering violations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,16 @@ export function RunDashboard() {
|
||||
const [showEndRun, setShowEndRun] = useState(false)
|
||||
const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
|
||||
|
||||
const encounters = run?.encounters ?? []
|
||||
const alive = useMemo(
|
||||
() => sortEncounters(encounters.filter((e) => e.status === 'caught' && e.faintLevel === null), teamSort),
|
||||
[encounters, teamSort],
|
||||
)
|
||||
const dead = useMemo(
|
||||
() => sortEncounters(encounters.filter((e) => e.status === 'caught' && e.faintLevel !== null), teamSort),
|
||||
[encounters, teamSort],
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
@@ -81,14 +91,6 @@ export function RunDashboard() {
|
||||
}
|
||||
|
||||
const isActive = run.status === 'active'
|
||||
const alive = useMemo(
|
||||
() => sortEncounters(run.encounters.filter((e) => e.status === 'caught' && e.faintLevel === null), teamSort),
|
||||
[run.encounters, teamSort],
|
||||
)
|
||||
const dead = useMemo(
|
||||
() => sortEncounters(run.encounters.filter((e) => e.status === 'caught' && e.faintLevel !== null), teamSort),
|
||||
[run.encounters, teamSort],
|
||||
)
|
||||
const visitedRoutes = new Set(run.encounters.map((e) => e.routeId)).size
|
||||
const totalRoutes = routes?.length
|
||||
|
||||
|
||||
@@ -33,6 +33,28 @@ import type {
|
||||
BossPokemon,
|
||||
} from '../types'
|
||||
|
||||
type TeamSortKey = 'route' | 'level' | 'species' | 'dex'
|
||||
|
||||
function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): EncounterDetail[] {
|
||||
return [...encounters].sort((a, b) => {
|
||||
switch (key) {
|
||||
case 'route':
|
||||
return a.route.order - b.route.order
|
||||
case 'level':
|
||||
return (a.catchLevel ?? 0) - (b.catchLevel ?? 0)
|
||||
case 'species': {
|
||||
const nameA = (a.currentPokemon ?? a.pokemon).name
|
||||
const nameB = (b.currentPokemon ?? b.pokemon).name
|
||||
return nameA.localeCompare(nameB)
|
||||
}
|
||||
case 'dex':
|
||||
return (a.currentPokemon ?? a.pokemon).nationalDex - (b.currentPokemon ?? b.pokemon).nationalDex
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const statusStyles: Record<RunStatus, string> = {
|
||||
active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
|
||||
completed:
|
||||
@@ -421,6 +443,7 @@ export function RunEncounters() {
|
||||
const [showEggModal, setShowEggModal] = useState(false)
|
||||
const [expandedBosses, setExpandedBosses] = useState<Set<number>>(new Set())
|
||||
const [showTeam, setShowTeam] = useState(true)
|
||||
const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
|
||||
const [filter, setFilter] = useState<'all' | RouteStatus>('all')
|
||||
|
||||
const storageKey = `expandedGroups-${runId}`
|
||||
@@ -621,10 +644,21 @@ export function RunEncounters() {
|
||||
}, [organizedRoutes, encounterByRoute]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const alive = useMemo(
|
||||
() => [...normalEncounters, ...transferEncounters, ...shinyEncounters].filter(
|
||||
(e) => e.status === 'caught' && e.faintLevel === null,
|
||||
() => sortEncounters(
|
||||
[...normalEncounters, ...transferEncounters, ...shinyEncounters].filter(
|
||||
(e) => e.status === 'caught' && e.faintLevel === null,
|
||||
),
|
||||
teamSort,
|
||||
),
|
||||
[normalEncounters, transferEncounters, shinyEncounters],
|
||||
[normalEncounters, transferEncounters, shinyEncounters, teamSort],
|
||||
)
|
||||
|
||||
const dead = useMemo(
|
||||
() => sortEncounters(
|
||||
normalEncounters.filter((e) => e.status === 'caught' && e.faintLevel !== null),
|
||||
teamSort,
|
||||
),
|
||||
[normalEncounters, teamSort],
|
||||
)
|
||||
|
||||
// Resolve HoF team encounters from IDs
|
||||
@@ -686,9 +720,6 @@ export function RunEncounters() {
|
||||
}
|
||||
|
||||
const isActive = run.status === 'active'
|
||||
const dead = normalEncounters.filter(
|
||||
(e) => e.status === 'caught' && e.faintLevel !== null,
|
||||
)
|
||||
|
||||
const toggleGroup = (groupId: number) => {
|
||||
updateExpandedGroups((prev) => {
|
||||
@@ -1036,31 +1067,45 @@ export function RunEncounters() {
|
||||
{/* Team Section */}
|
||||
{(alive.length > 0 || dead.length > 0) && (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTeam(!showTeam)}
|
||||
className="flex items-center gap-2 mb-3 group"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{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` : ''}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${showTeam ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTeam(!showTeam)}
|
||||
className="flex items-center gap-2 group"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{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` : ''}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${showTeam ? '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"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{showTeam && alive.length > 1 && (
|
||||
<select
|
||||
value={teamSort}
|
||||
onChange={(e) => setTeamSort(e.target.value as TeamSortKey)}
|
||||
className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<option value="route">Route Order</option>
|
||||
<option value="level">Catch Level</option>
|
||||
<option value="species">Species Name</option>
|
||||
<option value="dex">National Dex</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
{showTeam && (
|
||||
<>
|
||||
{alive.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user