2026-02-05 18:36:08 +01:00
|
|
|
import { useState } from 'react'
|
2026-02-05 15:28:50 +01:00
|
|
|
import { useParams, Link } from 'react-router-dom'
|
|
|
|
|
import { useRun } from '../hooks/useRuns'
|
|
|
|
|
import { useGameRoutes } from '../hooks/useGames'
|
2026-02-05 18:36:08 +01:00
|
|
|
import { useUpdateEncounter } from '../hooks/useEncounters'
|
|
|
|
|
import { StatCard, PokemonCard, RuleBadges, StatusChangeModal } from '../components'
|
|
|
|
|
import type { RunStatus, EncounterDetail } from '../types'
|
2026-02-05 15:28:50 +01:00
|
|
|
|
|
|
|
|
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 }>()
|
2026-02-05 18:36:08 +01:00
|
|
|
const runIdNum = Number(runId)
|
|
|
|
|
const { data: run, isLoading, error } = useRun(runIdNum)
|
2026-02-05 15:28:50 +01:00
|
|
|
const { data: routes } = useGameRoutes(run?.gameId ?? null)
|
2026-02-05 18:36:08 +01:00
|
|
|
const updateEncounter = useUpdateEncounter(runIdNum)
|
|
|
|
|
const [selectedEncounter, setSelectedEncounter] =
|
|
|
|
|
useState<EncounterDetail | null>(null)
|
2026-02-05 15:28:50 +01:00
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
>
|
|
|
|
|
← 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} · {run.game.region} · 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) => (
|
2026-02-05 18:36:08 +01:00
|
|
|
<PokemonCard
|
|
|
|
|
key={enc.id}
|
|
|
|
|
encounter={enc}
|
|
|
|
|
onClick={() => setSelectedEncounter(enc)}
|
|
|
|
|
/>
|
2026-02-05 15:28:50 +01:00
|
|
|
))}
|
|
|
|
|
</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) => (
|
2026-02-05 18:36:08 +01:00
|
|
|
<PokemonCard
|
|
|
|
|
key={enc.id}
|
|
|
|
|
encounter={enc}
|
|
|
|
|
showFaintLevel
|
|
|
|
|
onClick={() => setSelectedEncounter(enc)}
|
|
|
|
|
/>
|
2026-02-05 15:28:50 +01:00
|
|
|
))}
|
|
|
|
|
</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>
|
2026-02-05 18:36:08 +01:00
|
|
|
|
|
|
|
|
{/* Status Change Modal */}
|
|
|
|
|
{selectedEncounter && (
|
|
|
|
|
<StatusChangeModal
|
|
|
|
|
encounter={selectedEncounter}
|
|
|
|
|
onUpdate={(data) => {
|
|
|
|
|
updateEncounter.mutate(data, {
|
|
|
|
|
onSuccess: () => setSelectedEncounter(null),
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
onClose={() => setSelectedEncounter(null)}
|
|
|
|
|
isPending={updateEncounter.isPending}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-02-05 15:28:50 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|