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 [showEndRun, setShowEndRun] = useState(false)
|
||||||
const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-16">
|
<div className="flex items-center justify-center py-16">
|
||||||
@@ -81,14 +91,6 @@ export function RunDashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isActive = run.status === 'active'
|
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 visitedRoutes = new Set(run.encounters.map((e) => e.routeId)).size
|
||||||
const totalRoutes = routes?.length
|
const totalRoutes = routes?.length
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,28 @@ import type {
|
|||||||
BossPokemon,
|
BossPokemon,
|
||||||
} from '../types'
|
} 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> = {
|
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:
|
completed:
|
||||||
@@ -421,6 +443,7 @@ export function RunEncounters() {
|
|||||||
const [showEggModal, setShowEggModal] = useState(false)
|
const [showEggModal, setShowEggModal] = useState(false)
|
||||||
const [expandedBosses, setExpandedBosses] = useState<Set<number>>(new Set())
|
const [expandedBosses, setExpandedBosses] = useState<Set<number>>(new Set())
|
||||||
const [showTeam, setShowTeam] = useState(true)
|
const [showTeam, setShowTeam] = useState(true)
|
||||||
|
const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
|
||||||
const [filter, setFilter] = useState<'all' | RouteStatus>('all')
|
const [filter, setFilter] = useState<'all' | RouteStatus>('all')
|
||||||
|
|
||||||
const storageKey = `expandedGroups-${runId}`
|
const storageKey = `expandedGroups-${runId}`
|
||||||
@@ -621,10 +644,21 @@ export function RunEncounters() {
|
|||||||
}, [organizedRoutes, encounterByRoute]) // eslint-disable-line react-hooks/exhaustive-deps
|
}, [organizedRoutes, encounterByRoute]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const alive = useMemo(
|
const alive = useMemo(
|
||||||
() => [...normalEncounters, ...transferEncounters, ...shinyEncounters].filter(
|
() => sortEncounters(
|
||||||
(e) => e.status === 'caught' && e.faintLevel === null,
|
[...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
|
// Resolve HoF team encounters from IDs
|
||||||
@@ -686,9 +720,6 @@ export function RunEncounters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isActive = run.status === 'active'
|
const isActive = run.status === 'active'
|
||||||
const dead = normalEncounters.filter(
|
|
||||||
(e) => e.status === 'caught' && e.faintLevel !== null,
|
|
||||||
)
|
|
||||||
|
|
||||||
const toggleGroup = (groupId: number) => {
|
const toggleGroup = (groupId: number) => {
|
||||||
updateExpandedGroups((prev) => {
|
updateExpandedGroups((prev) => {
|
||||||
@@ -1036,31 +1067,45 @@ export function RunEncounters() {
|
|||||||
{/* Team Section */}
|
{/* Team Section */}
|
||||||
{(alive.length > 0 || dead.length > 0) && (
|
{(alive.length > 0 || dead.length > 0) && (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<button
|
<div className="flex items-center justify-between mb-3">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setShowTeam(!showTeam)}
|
type="button"
|
||||||
className="flex items-center gap-2 mb-3 group"
|
onClick={() => setShowTeam(!showTeam)}
|
||||||
>
|
className="flex items-center gap-2 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"
|
|
||||||
>
|
>
|
||||||
<path
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
strokeLinecap="round"
|
{isActive ? 'Team' : 'Final Team'}
|
||||||
strokeLinejoin="round"
|
</h2>
|
||||||
strokeWidth={2}
|
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
d="M19 9l-7 7-7-7"
|
{alive.length} alive{dead.length > 0 ? `, ${dead.length} dead` : ''}
|
||||||
/>
|
</span>
|
||||||
</svg>
|
<svg
|
||||||
</button>
|
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 && (
|
{showTeam && (
|
||||||
<>
|
<>
|
||||||
{alive.length > 0 && (
|
{alive.length > 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user