+
+ {title}
+
+ {pokemon.length === 0 ? (
+
No data
+ ) : (
+ <>
+
+ {visible.map((p, i) => (
+
+
+ {i + 1}.
+
+ {p.spriteUrl ? (
+

+ ) : (
+
+ )}
+
+ {p.name}
+
+
+ {p.count}
+
+
+ ))}
+
+ {pokemon.length > 5 && (
+
+ )}
+ >
+ )}
+
+ )
+}
+
+function HorizontalBar({
+ label,
+ value,
+ max,
+ color,
+ colorHex,
+}: {
+ label: string
+ value: number
+ max: number
+ color?: string
+ colorHex?: string
+}) {
+ const width = max > 0 ? (value / max) * 100 : 0
+ return (
+
+ {/* Run Overview */}
+
+
+
+
+
+
+
+
+
+ Win Rate: {pct(stats.winRate)}
+
+
+ Avg Duration: {fmt(stats.avgDurationDays, ' days')}
+
+
+
+
+ {/* Runs by Game */}
+ {stats.runsByGame.length > 0 && (
+
+
+ {stats.runsByGame.map((g) => (
+
+ ))}
+
+
+ )}
+
+ {/* Encounter Stats */}
+
+
+
+
+
+
+
+
+ Catch Rate: {pct(stats.catchRate)}
+
+
+ Avg per Run: {fmt(stats.avgEncountersPerRun)}
+
+
+
+
+ {/* Pokemon Rankings */}
+
+
+ {/* Team & Deaths */}
+
+
+
+
+
+ {pct(stats.mortalityRate)}
+
+
Mortality Rate
+
+
+
+ {fmt(stats.avgCatchLevel)}
+
+
Avg Catch Lv.
+
+
+
+ {fmt(stats.avgFaintLevel)}
+
+
Avg Faint Lv.
+
+
+
+ {stats.topDeathCauses.length > 0 && (
+
+
+ Top Death Causes
+
+
+ {stats.topDeathCauses.map((d, i) => (
+
+
+ {i + 1}.
+
+
+ {d.cause}
+
+
+ {d.count}
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Type Distribution */}
+ {stats.typeDistribution.length > 0 && (
+
+
+ {stats.typeDistribution.map((t) => (
+
+ ))}
+
+
+ )}
+
+ )
+}
+
+export function Stats() {
+ const { data: stats, isLoading, error } = useStats()
+
+ return (
+
+
+ Stats
+
+
+ {isLoading && (
+
+ )}
+
+ {error && (
+
+ Failed to load stats: {error.message}
+
+ )}
+
+ {stats && stats.totalRuns === 0 && (
+
+
No data yet
+
Start a Nuzlocke run to see your stats here.
+
+ )}
+
+ {stats && stats.totalRuns > 0 &&
}
+
+ )
+}
diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts
index 67b20d8..cbf12be 100644
--- a/frontend/src/pages/index.ts
+++ b/frontend/src/pages/index.ts
@@ -2,3 +2,4 @@ export { Home } from './Home'
export { NewRun } from './NewRun'
export { RunList } from './RunList'
export { RunEncounters } from './RunEncounters'
+export { Stats } from './Stats'
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 14fc022..1a52dc1 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -1,3 +1,4 @@
export * from './admin'
export * from './game'
export * from './rules'
+export * from './stats'
diff --git a/frontend/src/types/stats.ts b/frontend/src/types/stats.ts
new file mode 100644
index 0000000..c4b7fd1
--- /dev/null
+++ b/frontend/src/types/stats.ts
@@ -0,0 +1,51 @@
+export interface GameRunCount {
+ gameId: number
+ gameName: string
+ gameColor: string | null
+ count: number
+}
+
+export interface PokemonRanking {
+ pokemonId: number
+ name: string
+ spriteUrl: string | null
+ count: number
+}
+
+export interface DeathCause {
+ cause: string
+ count: number
+}
+
+export interface TypeCount {
+ type: string
+ count: number
+}
+
+export interface StatsResponse {
+ totalRuns: number
+ activeRuns: number
+ completedRuns: number
+ failedRuns: number
+ winRate: number | null
+ avgDurationDays: number | null
+
+ runsByGame: GameRunCount[]
+
+ totalEncounters: number
+ caughtCount: number
+ faintedCount: number
+ missedCount: number
+ catchRate: number | null
+ avgEncountersPerRun: number | null
+
+ topCaughtPokemon: PokemonRanking[]
+ topEncounteredPokemon: PokemonRanking[]
+
+ totalDeaths: number
+ mortalityRate: number | null
+ topDeathCauses: DeathCause[]
+ avgCatchLevel: number | null
+ avgFaintLevel: number | null
+ typeDistribution: TypeCount[]
+}