Add sort options to run team overview
Add a dropdown to sort Active Team and Graveyard by route order, catch level, species name, or national dex number. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
# nuzlocke-tracker-94hx
|
||||||
|
title: Add sort options to run team overview
|
||||||
|
status: completed
|
||||||
|
type: feature
|
||||||
|
priority: normal
|
||||||
|
created_at: 2026-02-09T10:03:49Z
|
||||||
|
updated_at: 2026-02-09T11:09:33Z
|
||||||
|
---
|
||||||
|
|
||||||
|
The Active Team / Final Team section on the run dashboard (`RunDashboard.tsx`) currently displays Pokemon in whatever order encounters arrive from the backend — there is no explicit sorting. Add a sort dropdown so the user can choose how their team is ordered.
|
||||||
|
|
||||||
|
## Sort options
|
||||||
|
- **Route order** (default) — sort by the route's `order` field, matching game progression
|
||||||
|
- **Catch level** — sort by `catchLevel`, ascending
|
||||||
|
- **Species name** — sort alphabetically by the display Pokemon's name (accounting for evolutions via `currentPokemon`)
|
||||||
|
- **National Dex** — sort by the display Pokemon's `nationalDex` number
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Frontend-only change — all data is already available in the `EncounterDetail` objects
|
||||||
|
- Add a small sort control (dropdown or segmented buttons) above the team grid
|
||||||
|
- Persist the selected sort in component state (no need for localStorage)
|
||||||
|
- Apply the same sort options to both the Active Team and Graveyard sections
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [x] Add sort state and sort logic to `RunDashboard.tsx`
|
||||||
|
- [x] Add sort dropdown/control above the team grid
|
||||||
|
- [x] Apply sorting to both `alive` and `dead` encounter arrays
|
||||||
|
- [x] Verify sort works correctly with evolved Pokemon (use `currentPokemon ?? pokemon` for name/dex)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useParams, Link } from 'react-router-dom'
|
import { useParams, Link } from 'react-router-dom'
|
||||||
import { useRun, useUpdateRun } from '../hooks/useRuns'
|
import { useRun, useUpdateRun } from '../hooks/useRuns'
|
||||||
import { useGameRoutes } from '../hooks/useGames'
|
import { useGameRoutes } from '../hooks/useGames'
|
||||||
@@ -6,6 +6,28 @@ import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
|||||||
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
||||||
import type { RunStatus, EncounterDetail } from '../types'
|
import type { RunStatus, EncounterDetail } 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:
|
||||||
@@ -32,6 +54,7 @@ export function RunDashboard() {
|
|||||||
const [selectedEncounter, setSelectedEncounter] =
|
const [selectedEncounter, setSelectedEncounter] =
|
||||||
useState<EncounterDetail | null>(null)
|
useState<EncounterDetail | null>(null)
|
||||||
const [showEndRun, setShowEndRun] = useState(false)
|
const [showEndRun, setShowEndRun] = useState(false)
|
||||||
|
const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -58,11 +81,13 @@ export function RunDashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isActive = run.status === 'active'
|
const isActive = run.status === 'active'
|
||||||
const alive = run.encounters.filter(
|
const alive = useMemo(
|
||||||
(e) => e.status === 'caught' && e.faintLevel === null,
|
() => sortEncounters(run.encounters.filter((e) => e.status === 'caught' && e.faintLevel === null), teamSort),
|
||||||
|
[run.encounters, teamSort],
|
||||||
)
|
)
|
||||||
const dead = run.encounters.filter(
|
const dead = useMemo(
|
||||||
(e) => e.status === 'caught' && e.faintLevel !== null,
|
() => 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
|
||||||
@@ -172,9 +197,23 @@ export function RunDashboard() {
|
|||||||
|
|
||||||
{/* Active Team */}
|
{/* Active Team */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
{isActive ? 'Active Team' : 'Final Team'}
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
</h2>
|
{isActive ? 'Active Team' : 'Final Team'}
|
||||||
|
</h2>
|
||||||
|
{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>
|
||||||
{alive.length === 0 ? (
|
{alive.length === 0 ? (
|
||||||
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
No pokemon caught yet — head to encounters to start building your
|
No pokemon caught yet — head to encounters to start building your
|
||||||
|
|||||||
Reference in New Issue
Block a user