Align repo config with global development standards
- Add missing tsconfig strictness flags (noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noPropertyAccessFromIndexSignature) and fix all resulting type errors - Replace ESLint/Prettier with oxlint 1.48.0 and oxfmt 0.33.0 - Pin all frontend and backend dependencies to exact versions - Pin GitHub Actions to SHA hashes with persist-credentials: false - Fix CI Python version mismatch (3.12 -> 3.14) and ruff target-version - Add vitest 4.0.18 with jsdom environment for frontend testing - Add ty 0.0.17 for Python type checking (non-blocking in CI) - Add actionlint and zizmor CI job for workflow linting and security audit - Add Dependabot config for npm, pip, and github-actions - Update CLAUDE.md and pre-commit hooks to reflect new tooling - Ignore Claude Code sandbox artifacts in gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,47 +34,27 @@ function pct(value: number | null): string {
|
||||
return `${(value * 100).toFixed(1)}%`
|
||||
}
|
||||
|
||||
function PokemonList({
|
||||
title,
|
||||
pokemon,
|
||||
}: {
|
||||
title: string
|
||||
pokemon: PokemonRanking[]
|
||||
}) {
|
||||
function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRanking[] }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const visible = expanded ? pokemon : pokemon.slice(0, 5)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">{title}</h3>
|
||||
{pokemon.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No data</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
{visible.map((p, i) => (
|
||||
<div
|
||||
key={p.pokemonId}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span className="text-gray-400 dark:text-gray-500 w-5 text-right">
|
||||
{i + 1}.
|
||||
</span>
|
||||
<div key={p.pokemonId} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-400 dark:text-gray-500 w-5 text-right">{i + 1}.</span>
|
||||
{p.spriteUrl ? (
|
||||
<img
|
||||
src={p.spriteUrl}
|
||||
alt={p.name}
|
||||
className="w-6 h-6"
|
||||
loading="lazy"
|
||||
/>
|
||||
<img src={p.spriteUrl} alt={p.name} className="w-6 h-6" loading="lazy" />
|
||||
) : (
|
||||
<div className="w-6 h-6 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
)}
|
||||
<span className="capitalize text-gray-800 dark:text-gray-200">
|
||||
{p.name}
|
||||
</span>
|
||||
<span className="capitalize text-gray-800 dark:text-gray-200">{p.name}</span>
|
||||
<span className="ml-auto text-gray-500 dark:text-gray-400 font-medium">
|
||||
{p.count}
|
||||
</span>
|
||||
@@ -130,14 +110,10 @@ function HorizontalBar({
|
||||
/>
|
||||
<span
|
||||
className={`absolute inset-0 flex items-center px-3 text-xs font-medium capitalize truncate ${
|
||||
isLight
|
||||
? 'text-gray-900 dark:text-gray-900'
|
||||
: 'text-gray-700 dark:text-gray-200'
|
||||
isLight ? 'text-gray-900 dark:text-gray-900' : 'text-gray-700 dark:text-gray-200'
|
||||
}`}
|
||||
style={{
|
||||
textShadow: isLight
|
||||
? '0 0 4px rgba(255,255,255,0.8)'
|
||||
: '0 0 4px rgba(0,0,0,0.3)',
|
||||
textShadow: isLight ? '0 0 4px rgba(255,255,255,0.8)' : '0 0 4px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
@@ -150,18 +126,10 @@ function HorizontalBar({
|
||||
)
|
||||
}
|
||||
|
||||
function Section({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-4">
|
||||
{title}
|
||||
</h2>
|
||||
<h2 className="text-lg font-bold text-gray-900 dark:text-gray-100 mb-4">{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
@@ -178,19 +146,13 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
|
||||
<StatCard label="Total Runs" value={stats.totalRuns} color="blue" />
|
||||
<StatCard label="Active" value={stats.activeRuns} color="green" />
|
||||
<StatCard
|
||||
label="Completed"
|
||||
value={stats.completedRuns}
|
||||
color="blue"
|
||||
/>
|
||||
<StatCard label="Completed" value={stats.completedRuns} color="blue" />
|
||||
<StatCard label="Failed" value={stats.failedRuns} color="red" />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>
|
||||
Win Rate:{' '}
|
||||
<strong className="text-gray-800 dark:text-gray-200">
|
||||
{pct(stats.winRate)}
|
||||
</strong>
|
||||
<strong className="text-gray-800 dark:text-gray-200">{pct(stats.winRate)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
Avg Duration:{' '}
|
||||
@@ -211,8 +173,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
label={g.gameName}
|
||||
value={g.count}
|
||||
max={gameMax}
|
||||
colorHex={g.gameColor ?? undefined}
|
||||
color={g.gameColor ? undefined : 'bg-blue-500'}
|
||||
{...(g.gameColor ? { colorHex: g.gameColor } : { color: 'bg-blue-500' })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -244,9 +205,7 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>
|
||||
Catch Rate:{' '}
|
||||
<strong className="text-gray-800 dark:text-gray-200">
|
||||
{pct(stats.catchRate)}
|
||||
</strong>
|
||||
<strong className="text-gray-800 dark:text-gray-200">{pct(stats.catchRate)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
Avg per Run:{' '}
|
||||
@@ -261,44 +220,31 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
<Section title="Pokemon Rankings">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<PokemonList title="Most Caught" pokemon={stats.topCaughtPokemon} />
|
||||
<PokemonList
|
||||
title="Most Encountered"
|
||||
pokemon={stats.topEncounteredPokemon}
|
||||
/>
|
||||
<PokemonList title="Most Encountered" pokemon={stats.topEncounteredPokemon} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Team & Deaths */}
|
||||
<Section title="Team & Deaths">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">
|
||||
<StatCard
|
||||
label="Total Deaths"
|
||||
value={stats.totalDeaths}
|
||||
color="red"
|
||||
/>
|
||||
<StatCard label="Total Deaths" value={stats.totalDeaths} color="red" />
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 border-amber-500">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{pct(stats.mortalityRate)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Mortality Rate
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Mortality Rate</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 border-blue-500">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{fmt(stats.avgCatchLevel)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Avg Catch Lv.
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Catch Lv.</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 border-l-4 border-purple-500">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{fmt(stats.avgFaintLevel)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Avg Faint Lv.
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Avg Faint Lv.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -310,12 +256,8 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
<div className="space-y-1.5">
|
||||
{stats.topDeathCauses.map((d, i) => (
|
||||
<div key={d.cause} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-gray-400 dark:text-gray-500 w-5 text-right">
|
||||
{i + 1}.
|
||||
</span>
|
||||
<span className="text-gray-800 dark:text-gray-200">
|
||||
{d.cause}
|
||||
</span>
|
||||
<span className="text-gray-400 dark:text-gray-500 w-5 text-right">{i + 1}.</span>
|
||||
<span className="text-gray-800 dark:text-gray-200">{d.cause}</span>
|
||||
<span className="ml-auto text-gray-500 dark:text-gray-400 font-medium">
|
||||
{d.count}
|
||||
</span>
|
||||
@@ -351,9 +293,7 @@ export function Stats() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-6">
|
||||
Stats
|
||||
</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-6">Stats</h1>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center py-12">
|
||||
@@ -370,9 +310,7 @@ export function Stats() {
|
||||
{stats && stats.totalRuns === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<p className="text-lg mb-2">No data yet</p>
|
||||
<p className="text-sm">
|
||||
Start a Nuzlocke run to see your stats here.
|
||||
</p>
|
||||
<p className="text-sm">Start a Nuzlocke run to see your stats here.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user