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 { useRun, useUpdateRun } from '../hooks/useRuns'
|
||||
import { useGameRoutes } from '../hooks/useGames'
|
||||
@@ -6,6 +6,28 @@ import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters'
|
||||
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components'
|
||||
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> = {
|
||||
active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
|
||||
completed:
|
||||
@@ -32,6 +54,7 @@ export function RunDashboard() {
|
||||
const [selectedEncounter, setSelectedEncounter] =
|
||||
useState<EncounterDetail | null>(null)
|
||||
const [showEndRun, setShowEndRun] = useState(false)
|
||||
const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -58,11 +81,13 @@ export function RunDashboard() {
|
||||
}
|
||||
|
||||
const isActive = run.status === 'active'
|
||||
const alive = run.encounters.filter(
|
||||
(e) => e.status === 'caught' && e.faintLevel === null,
|
||||
const alive = useMemo(
|
||||
() => sortEncounters(run.encounters.filter((e) => e.status === 'caught' && e.faintLevel === null), teamSort),
|
||||
[run.encounters, teamSort],
|
||||
)
|
||||
const dead = run.encounters.filter(
|
||||
(e) => e.status === 'caught' && e.faintLevel !== null,
|
||||
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
|
||||
@@ -172,9 +197,23 @@ export function RunDashboard() {
|
||||
|
||||
{/* Active Team */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||
{isActive ? 'Active Team' : 'Final Team'}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{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 ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||
No pokemon caught yet — head to encounters to start building your
|
||||
|
||||
Reference in New Issue
Block a user