Add run dashboard and encounter tracking interface

Run list at /runs shows all runs with status badges. Run dashboard at
/runs/:id displays stats, active team, graveyard, and rule badges.
Encounter tracking at /runs/:runId/encounters shows route list with
status indicators, progress bar, filters, and a modal for logging or
editing encounters with pokemon picker.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-05 15:28:50 +01:00
parent 982154b348
commit 66b3c9286f
19 changed files with 1054 additions and 63 deletions

View File

@@ -0,0 +1,152 @@
import { useParams, Link } from 'react-router-dom'
import { useRun } from '../hooks/useRuns'
import { useGameRoutes } from '../hooks/useGames'
import { StatCard, PokemonCard, RuleBadges } from '../components'
import type { RunStatus } from '../types'
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',
failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300',
}
export function RunDashboard() {
const { runId } = useParams<{ runId: string }>()
const { data: run, isLoading, error } = useRun(Number(runId))
const { data: routes } = useGameRoutes(run?.gameId ?? null)
if (isLoading) {
return (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
)
}
if (error || !run) {
return (
<div className="max-w-4xl mx-auto p-8">
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
Failed to load run. It may not exist.
</div>
<Link
to="/runs"
className="inline-block mt-4 text-blue-600 hover:underline"
>
Back to runs
</Link>
</div>
)
}
const alive = run.encounters.filter(
(e) => e.status === 'caught' && e.faintLevel === null,
)
const dead = run.encounters.filter(
(e) => e.status === 'caught' && e.faintLevel !== null,
)
const visitedRoutes = new Set(run.encounters.map((e) => e.routeId)).size
const totalRoutes = routes?.length
return (
<div className="max-w-4xl mx-auto p-8">
{/* Header */}
<div className="mb-6">
<Link
to="/runs"
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-2 inline-block"
>
&larr; All Runs
</Link>
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
{run.name}
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
{run.game.name} &middot; {run.game.region} &middot; Started{' '}
{new Date(run.startedAt).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</p>
</div>
<span
className={`px-3 py-1 rounded-full text-sm font-medium capitalize ${statusStyles[run.status]}`}
>
{run.status}
</span>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
<StatCard
label="Encounters"
value={run.encounters.length}
color="blue"
/>
<StatCard label="Alive" value={alive.length} color="green" />
<StatCard label="Deaths" value={dead.length} color="red" />
<StatCard
label="Routes Visited"
value={visitedRoutes}
total={totalRoutes}
color="purple"
/>
</div>
{/* Rules */}
<div className="mb-6">
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
Active Rules
</h2>
<RuleBadges rules={run.rules} />
</div>
{/* Active Team */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
Active Team
</h2>
{alive.length === 0 ? (
<p className="text-gray-500 dark:text-gray-400 text-sm">
No pokemon caught yet
</p>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{alive.map((enc) => (
<PokemonCard key={enc.id} encounter={enc} />
))}
</div>
)}
</div>
{/* Graveyard */}
{dead.length > 0 && (
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
Graveyard
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{dead.map((enc) => (
<PokemonCard key={enc.id} encounter={enc} showFaintLevel />
))}
</div>
</div>
)}
{/* Quick Actions */}
<div className="mt-8">
<Link
to={`/runs/${runId}/encounters`}
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
>
Log Encounter
</Link>
</div>
</div>
)
}