Implement dark-first design system with Geist typography (#28)
Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
This commit was merged in pull request #28.
This commit is contained in:
@@ -12,7 +12,7 @@ const statusColors: Record<RunStatus, string> = {
|
||||
}
|
||||
|
||||
const statusRing: Record<RunStatus, string> = {
|
||||
completed: 'ring-blue-500',
|
||||
completed: 'ring-accent-500',
|
||||
active: 'ring-green-500 animate-pulse',
|
||||
failed: 'ring-red-500',
|
||||
}
|
||||
@@ -32,18 +32,16 @@ function LegIndicator({ leg }: { leg: GenlockeLegDetail }) {
|
||||
className={`w-4 h-4 rounded-full ${statusColors[status]} ring-2 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 ${statusRing[status]}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600" />
|
||||
<div className="w-4 h-4 rounded-full bg-surface-3" />
|
||||
)
|
||||
|
||||
const content = (
|
||||
<div className="flex flex-col items-center gap-1 min-w-[80px]">
|
||||
{dot}
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300 text-center leading-tight">
|
||||
<span className="text-xs font-medium text-text-secondary text-center leading-tight">
|
||||
{leg.game.name}
|
||||
</span>
|
||||
{status && (
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 capitalize">{status}</span>
|
||||
)}
|
||||
{status && <span className="text-[10px] text-text-tertiary capitalize">{status}</span>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -72,7 +70,7 @@ function PokemonSprite({ pokemon }: { pokemon: RetiredPokemon }) {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold"
|
||||
className="w-10 h-10 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold"
|
||||
title={pokemon.name}
|
||||
>
|
||||
{pokemon.name[0]?.toUpperCase()}
|
||||
@@ -137,7 +135,7 @@ export function GenlockeDetail() {
|
||||
if (error || !genlocke) {
|
||||
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">
|
||||
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to load genlocke. Please try again.
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,11 +155,11 @@ export function GenlockeDetail() {
|
||||
<div className="max-w-4xl mx-auto p-8 space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<Link to="/genlockes" className="text-sm text-blue-600 dark:text-blue-400 hover:underline">
|
||||
<Link to="/genlockes" className="text-sm text-text-link hover:underline">
|
||||
← Back to Genlockes
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">{genlocke.name}</h1>
|
||||
<h1 className="text-3xl font-bold text-text-primary">{genlocke.name}</h1>
|
||||
<span
|
||||
className={`px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${statusStyles[genlocke.status]}`}
|
||||
>
|
||||
@@ -172,8 +170,8 @@ export function GenlockeDetail() {
|
||||
|
||||
{/* Progress Timeline */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Progress</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Progress</h2>
|
||||
<div className="bg-surface-1 rounded-lg shadow p-6">
|
||||
<div className="flex items-start gap-2 overflow-x-auto pb-2">
|
||||
{genlocke.legs.map((leg, i) => (
|
||||
<div key={leg.id} className="flex items-center">
|
||||
@@ -181,7 +179,7 @@ export function GenlockeDetail() {
|
||||
{i < genlocke.legs.length - 1 && (
|
||||
<div
|
||||
className={`h-0.5 w-6 mx-1 mt-[-16px] ${
|
||||
leg.runStatus === 'completed' ? 'bg-blue-500' : 'bg-gray-300 dark:bg-gray-600'
|
||||
leg.runStatus === 'completed' ? 'bg-blue-500' : 'bg-surface-3'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
@@ -193,9 +191,7 @@ export function GenlockeDetail() {
|
||||
|
||||
{/* Cumulative Stats */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Cumulative Stats
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Cumulative Stats</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<StatCard label="Encounters" value={genlocke.stats.totalEncounters} color="blue" />
|
||||
<StatCard label="Deaths" value={genlocke.stats.totalDeaths} color="red" />
|
||||
@@ -211,30 +207,24 @@ export function GenlockeDetail() {
|
||||
|
||||
{/* Configuration */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Configuration
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Configuration</h2>
|
||||
<div className="bg-surface-1 rounded-lg shadow p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Genlocke Rules
|
||||
</h3>
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">Genlocke Rules</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{genlocke.genlockeRules.retireHoF ? (
|
||||
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
|
||||
Retire HoF Teams
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm text-text-tertiary">
|
||||
No genlocke-specific rules enabled
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Nuzlocke Rules
|
||||
</h3>
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">Nuzlocke Rules</h3>
|
||||
<RuleBadges rules={genlocke.nuzlockeRules} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,13 +233,11 @@ export function GenlockeDetail() {
|
||||
{/* Retired Families */}
|
||||
{genlocke.genlockeRules.retireHoF && retiredByLeg.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Retired Families
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Retired Families</h2>
|
||||
<div className="space-y-3">
|
||||
{retiredByLeg.map((leg) => (
|
||||
<div key={leg.legOrder} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
<div key={leg.legOrder} className="bg-surface-1 rounded-lg shadow p-4">
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">
|
||||
Leg {leg.legOrder} — {leg.gameName}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
@@ -267,9 +255,7 @@ export function GenlockeDetail() {
|
||||
|
||||
{/* Quick Actions */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Quick Actions
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Quick Actions</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{activeLeg && (
|
||||
<Link
|
||||
@@ -284,7 +270,7 @@ export function GenlockeDetail() {
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
showGraveyard
|
||||
? 'bg-red-600 text-white hover:bg-red-700'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
: 'bg-surface-3 text-text-secondary hover:bg-surface-4'
|
||||
}`}
|
||||
>
|
||||
Graveyard
|
||||
@@ -293,8 +279,8 @@ export function GenlockeDetail() {
|
||||
onClick={() => setShowLineage((v) => !v)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
showLineage
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
? 'bg-accent-600 text-white hover:bg-accent-500'
|
||||
: 'bg-surface-3 text-text-secondary hover:bg-surface-4'
|
||||
}`}
|
||||
>
|
||||
Lineage
|
||||
@@ -305,9 +291,7 @@ export function GenlockeDetail() {
|
||||
{/* Graveyard */}
|
||||
{showGraveyard && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Cumulative Graveyard
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Cumulative Graveyard</h2>
|
||||
<GenlockeGraveyard genlockeId={id} />
|
||||
</section>
|
||||
)}
|
||||
@@ -315,9 +299,7 @@ export function GenlockeDetail() {
|
||||
{/* Lineage */}
|
||||
{showLineage && (
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Pokemon Lineages
|
||||
</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary mb-4">Pokemon Lineages</h2>
|
||||
<GenlockeLineage genlockeId={id} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -14,10 +14,10 @@ export function GenlockeList() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Your Genlockes</h1>
|
||||
<h1 className="text-3xl font-bold text-text-primary">Your Genlockes</h1>
|
||||
<Link
|
||||
to="/genlockes/new"
|
||||
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"
|
||||
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-accent-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Start New Genlocke
|
||||
</Link>
|
||||
@@ -30,14 +30,14 @@ export function GenlockeList() {
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to load genlockes. Please try again.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{genlockes && genlockes.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 mb-4">
|
||||
<p className="text-lg text-text-tertiary mb-4">
|
||||
No genlockes yet. Start your first Generation Locke!
|
||||
</p>
|
||||
<Link
|
||||
@@ -55,14 +55,12 @@ export function GenlockeList() {
|
||||
<Link
|
||||
key={g.id}
|
||||
to={`/genlockes/${g.id}`}
|
||||
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-4"
|
||||
className="block bg-surface-1 rounded-lg shadow hover:shadow-md transition-shadow p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{g.name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-lg font-semibold text-text-primary">{g.name}</h2>
|
||||
<p className="text-sm text-text-tertiary">
|
||||
{g.currentLegOrder !== null
|
||||
? `Leg ${g.currentLegOrder} / ${g.totalLegs}`
|
||||
: `${g.completedLegs} / ${g.totalLegs} legs completed`}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useRuns } from '../hooks/useRuns'
|
||||
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',
|
||||
active: 'bg-status-active-bg text-status-active border border-status-active/20',
|
||||
completed: 'bg-status-completed-bg text-status-completed border border-status-completed/20',
|
||||
failed: 'bg-status-failed-bg text-status-failed border border-status-failed/20',
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
@@ -16,42 +16,46 @@ export function Home() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-gray-100 mb-2">
|
||||
Another Nuzlocke Tracker
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
|
||||
Track your Nuzlocke runs with ease
|
||||
</p>
|
||||
<Link
|
||||
to="/runs/new"
|
||||
className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg font-medium text-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Start New Run
|
||||
</Link>
|
||||
{/* Hero */}
|
||||
<div className="relative text-center py-16 mb-8 overflow-hidden rounded-2xl bg-gradient-to-b from-surface-2 to-surface-0 border border-border-default">
|
||||
<img
|
||||
src="/favicon.svg"
|
||||
alt=""
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 opacity-[0.04] pointer-events-none select-none"
|
||||
/>
|
||||
<div className="relative">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight text-text-primary mb-3">
|
||||
Another Nuzlocke Tracker
|
||||
</h1>
|
||||
<p className="text-lg text-text-secondary mb-8">Track your Nuzlocke runs with ease</p>
|
||||
<Link
|
||||
to="/runs/new"
|
||||
className="inline-block px-6 py-3 bg-accent-600 text-white rounded-lg font-medium text-lg hover:bg-accent-500 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 focus:ring-offset-surface-0 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Start New Run
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="w-6 h-6 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-accent-400 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeRun && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
||||
<h2 className="text-xs font-medium text-text-tertiary uppercase tracking-wider mb-3">
|
||||
Continue Playing
|
||||
</h2>
|
||||
<Link
|
||||
to={`/runs/${activeRun.id}`}
|
||||
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-5 border-l-4 border-green-500"
|
||||
className="block bg-surface-1 rounded-xl border border-border-default hover:border-accent-400/30 transition-all hover:-translate-y-0.5 p-5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{activeRun.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<h3 className="text-lg font-semibold text-text-primary">{activeRun.name}</h3>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Started{' '}
|
||||
{new Date(activeRun.startedAt).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
@@ -60,9 +64,7 @@ export function Home() {
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-blue-600 dark:text-blue-400 font-medium text-sm">
|
||||
Resume →
|
||||
</span>
|
||||
<span className="text-accent-300 font-medium text-sm">Resume →</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -71,10 +73,13 @@ export function Home() {
|
||||
{recentRuns && recentRuns.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<h2 className="text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Recent Runs
|
||||
</h2>
|
||||
<Link to="/runs" className="text-sm text-blue-600 dark:text-blue-400 hover:underline">
|
||||
<Link
|
||||
to="/runs"
|
||||
className="text-sm text-text-link hover:text-accent-200 transition-colors"
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
</div>
|
||||
@@ -83,12 +88,12 @@ export function Home() {
|
||||
<Link
|
||||
key={run.id}
|
||||
to={`/runs/${run.id}`}
|
||||
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-4"
|
||||
className="block bg-surface-1 rounded-xl border border-border-default hover:border-border-accent transition-all hover:-translate-y-0.5 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100">{run.name}</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<h3 className="font-medium text-text-primary">{run.name}</h3>
|
||||
<p className="text-xs text-text-secondary">
|
||||
{new Date(run.startedAt).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
@@ -109,7 +114,7 @@ export function Home() {
|
||||
)}
|
||||
|
||||
{runs && runs.length === 0 && (
|
||||
<p className="text-center text-gray-500 dark:text-gray-400">
|
||||
<p className="text-center text-text-secondary">
|
||||
No runs yet. Start your first Nuzlocke challenge above!
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -116,21 +116,19 @@ export function NewGenlocke() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">New Genlocke</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">Set up your generational challenge.</p>
|
||||
<h1 className="text-3xl font-bold text-text-primary mb-2">New Genlocke</h1>
|
||||
<p className="text-text-tertiary mb-6">Set up your generational challenge.</p>
|
||||
|
||||
<StepIndicator currentStep={step} onStepClick={setStep} steps={STEPS} />
|
||||
|
||||
{/* Step 1: Name */}
|
||||
{step === 1 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Name Your Genlocke
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-semibold text-text-primary mb-4">Name Your Genlocke</h2>
|
||||
<div className="bg-surface-1 rounded-lg shadow p-6">
|
||||
<label
|
||||
htmlFor="genlocke-name"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
className="block text-sm font-medium text-text-secondary mb-1"
|
||||
>
|
||||
Genlocke Name
|
||||
</label>
|
||||
@@ -139,7 +137,7 @@ export function NewGenlocke() {
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-transparent"
|
||||
placeholder="My Genlocke"
|
||||
maxLength={100}
|
||||
autoFocus
|
||||
@@ -151,7 +149,7 @@ export function NewGenlocke() {
|
||||
type="button"
|
||||
disabled={!name.trim()}
|
||||
onClick={() => setStep(2)}
|
||||
className="px-6 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 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -162,9 +160,7 @@ export function NewGenlocke() {
|
||||
{/* Step 2: Game Selection */}
|
||||
{step === 2 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Select Games
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-text-primary mb-4">Select Games</h2>
|
||||
|
||||
{/* Preset buttons */}
|
||||
<div className="flex gap-3 mb-6">
|
||||
@@ -188,17 +184,15 @@ export function NewGenlocke() {
|
||||
className={`flex-1 p-4 rounded-lg border-2 text-left transition-colors ${
|
||||
isActive
|
||||
? 'border-blue-600 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
: 'border-border-default hover:border-border-default'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`font-medium ${isActive ? 'text-blue-600 dark:text-blue-400' : 'text-gray-900 dark:text-gray-100'}`}
|
||||
className={`font-medium ${isActive ? 'text-text-link' : 'text-text-primary'}`}
|
||||
>
|
||||
{labels[type]}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{descriptions[type]}
|
||||
</div>
|
||||
<div className="text-sm text-text-tertiary mt-1">{descriptions[type]}</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -212,7 +206,7 @@ export function NewGenlocke() {
|
||||
|
||||
{/* Legs list */}
|
||||
{legs.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="bg-surface-1 rounded-lg shadow divide-y divide-border-default">
|
||||
{legs.map((leg, index) => (
|
||||
<LegRow
|
||||
key={`${leg.region}-${index}`}
|
||||
@@ -246,7 +240,7 @@ export function NewGenlocke() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
@@ -254,7 +248,7 @@ export function NewGenlocke() {
|
||||
type="button"
|
||||
disabled={legs.length === 0}
|
||||
onClick={() => setStep(3)}
|
||||
className="px-6 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 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -268,18 +262,16 @@ export function NewGenlocke() {
|
||||
<RulesConfiguration rules={nuzlockeRules} onChange={setNuzlockeRules} />
|
||||
|
||||
{/* Genlocke-specific rules */}
|
||||
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Genlocke Rules
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="mt-6 bg-surface-1 rounded-lg shadow">
|
||||
<div className="px-4 py-3 border-b border-border-default">
|
||||
<h3 className="text-lg font-medium text-text-primary">Genlocke Rules</h3>
|
||||
<p className="text-sm text-text-tertiary">
|
||||
Rules specific to the generational challenge
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-4 py-4">
|
||||
<fieldset>
|
||||
<legend className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
<legend className="text-sm font-medium text-text-secondary mb-3">
|
||||
Hall of Fame Pokemon
|
||||
</legend>
|
||||
<div className="space-y-3">
|
||||
@@ -289,13 +281,11 @@ export function NewGenlocke() {
|
||||
name="hofRule"
|
||||
checked={!genlockeRules.retireHoF}
|
||||
onChange={() => setGenlockeRules({ retireHoF: false })}
|
||||
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-accent-400"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">
|
||||
Keep Hall of Fame
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="font-medium text-text-primary">Keep Hall of Fame</div>
|
||||
<div className="text-sm text-text-tertiary">
|
||||
Pokemon that beat the Elite Four can continue to the next leg
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,13 +296,11 @@ export function NewGenlocke() {
|
||||
name="hofRule"
|
||||
checked={genlockeRules.retireHoF}
|
||||
onChange={() => setGenlockeRules({ retireHoF: true })}
|
||||
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-blue-500"
|
||||
className="mt-0.5 w-4 h-4 text-blue-600 focus:ring-accent-400"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">
|
||||
Retire Hall of Fame
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="font-medium text-text-primary">Retire Hall of Fame</div>
|
||||
<div className="text-sm text-text-tertiary">
|
||||
Pokemon that beat the Elite Four are retired and cannot be used in the next
|
||||
leg
|
||||
</div>
|
||||
@@ -324,12 +312,10 @@ export function NewGenlocke() {
|
||||
</div>
|
||||
|
||||
{/* Naming scheme */}
|
||||
<div className="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Naming Scheme
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="mt-6 bg-surface-1 rounded-lg shadow">
|
||||
<div className="px-4 py-3 border-b border-border-default">
|
||||
<h3 className="text-lg font-medium text-text-primary">Naming Scheme</h3>
|
||||
<p className="text-sm text-text-tertiary">
|
||||
Get nickname suggestions from a themed word list when catching Pokemon. Applied to
|
||||
all legs.
|
||||
</p>
|
||||
@@ -338,7 +324,7 @@ export function NewGenlocke() {
|
||||
<select
|
||||
value={namingScheme ?? ''}
|
||||
onChange={(e) => setNamingScheme(e.target.value || null)}
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full max-w-xs px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400"
|
||||
>
|
||||
<option value="">None (manual nicknames)</option>
|
||||
{namingCategories?.map((cat) => (
|
||||
@@ -354,14 +340,14 @@ export function NewGenlocke() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(2)}
|
||||
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(4)}
|
||||
className="px-6 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"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -372,32 +358,26 @@ export function NewGenlocke() {
|
||||
{/* Step 4: Confirm */}
|
||||
{step === 4 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Confirm & Start
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-text-primary mb-4">Confirm & Start</h2>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
|
||||
<div className="bg-surface-1 rounded-lg shadow p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Name</h3>
|
||||
<p className="text-gray-900 dark:text-gray-100 font-medium">{name}</p>
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-1">Name</h3>
|
||||
<p className="text-text-primary font-medium">{name}</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Legs ({legs.length})
|
||||
</h3>
|
||||
<div className="border-t border-border-default pt-4">
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">Legs ({legs.length})</h3>
|
||||
<ol className="space-y-2">
|
||||
{legs.map((leg, i) => (
|
||||
<li key={i} className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 w-6 text-right font-mono">
|
||||
<span className="text-sm text-text-muted w-6 text-right font-mono">
|
||||
{i + 1}.
|
||||
</span>
|
||||
<GameThumb game={leg.game} />
|
||||
<div>
|
||||
<span className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{leg.game.name}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 ml-2">
|
||||
<span className="text-text-primary font-medium">{leg.game.name}</span>
|
||||
<span className="text-sm text-text-tertiary ml-2">
|
||||
{leg.region.charAt(0).toUpperCase() + leg.region.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -406,24 +386,24 @@ export function NewGenlocke() {
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Rules</h3>
|
||||
<div className="border-t border-border-default pt-4">
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-1">Rules</h3>
|
||||
<dl className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Nuzlocke Rules</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
<dt className="text-text-tertiary">Nuzlocke Rules</dt>
|
||||
<dd className="text-text-primary font-medium">
|
||||
{enabledRuleCount} of {totalRuleCount} enabled
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Hall of Fame</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
<dt className="text-text-tertiary">Hall of Fame</dt>
|
||||
<dd className="text-text-primary font-medium">
|
||||
{genlockeRules.retireHoF ? 'Retire' : 'Keep'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Naming Scheme</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
<dt className="text-text-tertiary">Naming Scheme</dt>
|
||||
<dd className="text-text-primary font-medium">
|
||||
{namingScheme
|
||||
? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1)
|
||||
: 'None'}
|
||||
@@ -434,7 +414,7 @@ export function NewGenlocke() {
|
||||
</div>
|
||||
|
||||
{createGenlocke.error && (
|
||||
<div className="mt-4 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
<div className="mt-4 rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to create genlocke. Please try again.
|
||||
</div>
|
||||
)}
|
||||
@@ -443,7 +423,7 @@ export function NewGenlocke() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(3)}
|
||||
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
@@ -451,7 +431,7 @@ export function NewGenlocke() {
|
||||
type="button"
|
||||
disabled={createGenlocke.isPending}
|
||||
onClick={handleCreate}
|
||||
className="px-6 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 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{createGenlocke.isPending ? 'Creating...' : 'Start Genlocke'}
|
||||
</button>
|
||||
@@ -486,12 +466,12 @@ function LegRow({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500 w-6 text-right font-mono shrink-0">
|
||||
<span className="text-sm text-text-muted w-6 text-right font-mono shrink-0">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<GameThumb game={leg.game} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="text-sm text-text-tertiary">
|
||||
{leg.region.charAt(0).toUpperCase() + leg.region.slice(1)}
|
||||
</div>
|
||||
{games.length > 1 ? (
|
||||
@@ -501,7 +481,7 @@ function LegRow({
|
||||
const game = games.find((g) => g.id === Number(e.target.value))
|
||||
if (game) onGameChange(game)
|
||||
}}
|
||||
className="mt-1 w-full max-w-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="mt-1 w-full max-w-xs px-2 py-1 rounded border border-border-default bg-surface-2 text-text-primary text-sm focus:outline-none focus:ring-2 focus:ring-accent-400"
|
||||
>
|
||||
{games.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
@@ -510,7 +490,7 @@ function LegRow({
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<div className="text-gray-900 dark:text-gray-100 font-medium">{leg.game.name}</div>
|
||||
<div className="text-text-primary font-medium">{leg.game.name}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
@@ -518,7 +498,7 @@ function LegRow({
|
||||
type="button"
|
||||
disabled={index === 0}
|
||||
onClick={() => onMove('up')}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
className="p-1 text-gray-400 hover:text-text-secondary disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Move up"
|
||||
>
|
||||
<svg
|
||||
@@ -535,7 +515,7 @@ function LegRow({
|
||||
type="button"
|
||||
disabled={index === total - 1}
|
||||
onClick={() => onMove('down')}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
className="p-1 text-gray-400 hover:text-text-secondary disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
title="Move down"
|
||||
>
|
||||
<svg
|
||||
@@ -583,7 +563,7 @@ function AddLegDropdown({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
||||
className="flex items-center gap-2 text-sm text-text-link hover:text-blue-700 dark:hover:text-blue-300 font-medium"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
@@ -600,10 +580,8 @@ function AddLegDropdown({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-3">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select a region to add
|
||||
</div>
|
||||
<div className="bg-surface-1 rounded-lg shadow p-3">
|
||||
<div className="text-sm font-medium text-text-secondary mb-2">Select a region to add</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{regions.map((region) => (
|
||||
<button
|
||||
@@ -613,7 +591,7 @@ function AddLegDropdown({
|
||||
onAdd(region)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="px-3 py-1.5 rounded-md text-sm bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
className="px-3 py-1.5 rounded-md text-sm bg-surface-2 text-text-primary hover:bg-surface-3 transition-colors"
|
||||
>
|
||||
{region.name.charAt(0).toUpperCase() + region.name.slice(1)}
|
||||
</button>
|
||||
@@ -621,7 +599,7 @@ function AddLegDropdown({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="px-3 py-1.5 rounded-md text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
className="px-3 py-1.5 rounded-md text-sm text-text-tertiary hover:text-text-secondary"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -57,27 +57,23 @@ export function NewRun() {
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">New Nuzlocke Run</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">Set up your run in a few steps.</p>
|
||||
<h1 className="text-3xl font-bold text-text-primary mb-2">New Nuzlocke Run</h1>
|
||||
<p className="text-text-tertiary mb-6">Set up your run in a few steps.</p>
|
||||
|
||||
<StepIndicator currentStep={step} onStepClick={setStep} />
|
||||
|
||||
{step === 1 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Choose a Game
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-text-primary mb-4">Choose a Game</h2>
|
||||
|
||||
<div className="sticky top-0 z-10 bg-gray-50 dark:bg-gray-900 py-3 mb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="sticky top-0 z-10 bg-surface-0 py-3 mb-4 border-b border-border-default">
|
||||
{selectedGame ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<SelectedGameThumb game={selectedGame} />
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{selectedGame.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<p className="font-medium text-text-primary">{selectedGame.name}</p>
|
||||
<p className="text-sm text-text-tertiary">
|
||||
{selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -85,16 +81,14 @@ export function NewRun() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(2)}
|
||||
className="px-6 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"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Select a game to continue
|
||||
</p>
|
||||
<p className="text-sm text-text-tertiary">Select a game to continue</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled
|
||||
@@ -113,7 +107,7 @@ export function NewRun() {
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to load games. Please try again.
|
||||
</div>
|
||||
)}
|
||||
@@ -137,14 +131,14 @@ export function NewRun() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(3)}
|
||||
className="px-6 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"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -154,15 +148,13 @@ export function NewRun() {
|
||||
|
||||
{step === 3 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Name Your Run
|
||||
</h2>
|
||||
<h2 className="text-xl font-semibold text-text-primary mb-4">Name Your Run</h2>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 space-y-4">
|
||||
<div className="bg-surface-1 rounded-lg shadow p-6 space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="run-name"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
className="block text-sm font-medium text-text-secondary mb-1"
|
||||
>
|
||||
Run Name
|
||||
</label>
|
||||
@@ -171,7 +163,7 @@ export function NewRun() {
|
||||
type="text"
|
||||
value={runName}
|
||||
onChange={(e) => setRunName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-transparent"
|
||||
placeholder="My Nuzlocke Run"
|
||||
/>
|
||||
</div>
|
||||
@@ -180,7 +172,7 @@ export function NewRun() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="naming-scheme"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
|
||||
className="block text-sm font-medium text-text-secondary mb-1"
|
||||
>
|
||||
Naming Scheme
|
||||
</label>
|
||||
@@ -188,7 +180,7 @@ export function NewRun() {
|
||||
id="naming-scheme"
|
||||
value={namingScheme ?? ''}
|
||||
onChange={(e) => setNamingScheme(e.target.value || null)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="w-full px-3 py-2 rounded-lg border border-border-default bg-surface-2 text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-transparent"
|
||||
>
|
||||
<option value="">None (manual nicknames)</option>
|
||||
{namingCategories.map((cat) => (
|
||||
@@ -197,37 +189,35 @@ export function NewRun() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-1 text-xs text-text-tertiary">
|
||||
Get nickname suggestions from a themed word list when catching Pokemon.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Summary</h3>
|
||||
<div className="border-t border-border-default pt-4">
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">Summary</h3>
|
||||
<dl className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Game</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
{selectedGame?.name}
|
||||
</dd>
|
||||
<dt className="text-text-tertiary">Game</dt>
|
||||
<dd className="text-text-primary font-medium">{selectedGame?.name}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Region</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
<dt className="text-text-tertiary">Region</dt>
|
||||
<dd className="text-text-primary font-medium">
|
||||
{selectedGame &&
|
||||
selectedGame.region.charAt(0).toUpperCase() + selectedGame.region.slice(1)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Rules</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
<dt className="text-text-tertiary">Rules</dt>
|
||||
<dd className="text-text-primary font-medium">
|
||||
{enabledRuleCount} of {totalRuleCount} enabled
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-600 dark:text-gray-400">Naming Scheme</dt>
|
||||
<dd className="text-gray-900 dark:text-gray-100 font-medium">
|
||||
<dt className="text-text-tertiary">Naming Scheme</dt>
|
||||
<dd className="text-text-primary font-medium">
|
||||
{namingScheme
|
||||
? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1)
|
||||
: 'None'}
|
||||
@@ -238,7 +228,7 @@ export function NewRun() {
|
||||
</div>
|
||||
|
||||
{createRun.error && (
|
||||
<div className="mt-4 rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
<div className="mt-4 rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to create run. Please try again.
|
||||
</div>
|
||||
)}
|
||||
@@ -247,7 +237,7 @@ export function NewRun() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(2)}
|
||||
className="px-6 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg font-medium hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
className="px-6 py-2 text-text-secondary bg-surface-2 rounded-lg font-medium hover:bg-surface-3 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
@@ -255,7 +245,7 @@ export function NewRun() {
|
||||
type="button"
|
||||
disabled={!runName.trim() || createRun.isPending}
|
||||
onClick={handleCreate}
|
||||
className="px-6 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 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{createRun.isPending ? 'Creating...' : 'Create Run'}
|
||||
</button>
|
||||
|
||||
@@ -86,7 +86,7 @@ export function RunDashboard() {
|
||||
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">
|
||||
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to load run. It may not exist.
|
||||
</div>
|
||||
<Link to="/runs" className="inline-block mt-4 text-blue-600 hover:underline">
|
||||
@@ -106,14 +106,14 @@ export function RunDashboard() {
|
||||
<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"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary 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">
|
||||
<h1 className="text-3xl font-bold text-text-primary">{run.name}</h1>
|
||||
<p className="text-text-tertiary mt-1">
|
||||
{run.game.name} ·{' '}
|
||||
{run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '}
|
||||
{new Date(run.startedAt).toLocaleDateString(undefined, {
|
||||
@@ -137,7 +137,7 @@ export function RunDashboard() {
|
||||
className={`rounded-lg p-4 mb-6 ${
|
||||
run.status === 'completed'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
|
||||
: 'bg-status-failed-bg border border-red-200 dark:border-red-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -156,9 +156,7 @@ export function RunDashboard() {
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm ${
|
||||
run.status === 'completed'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
run.status === 'completed' ? 'text-text-link' : 'text-status-failed'
|
||||
}`}
|
||||
>
|
||||
{run.completedAt && (
|
||||
@@ -189,21 +187,19 @@ export function RunDashboard() {
|
||||
|
||||
{/* Rules */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Rules</h2>
|
||||
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
|
||||
<RuleBadges rules={run.rules} />
|
||||
</div>
|
||||
|
||||
{/* Naming Scheme */}
|
||||
{namingCategories && namingCategories.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Naming Scheme
|
||||
</h2>
|
||||
<h2 className="text-sm font-medium text-text-tertiary mb-2">Naming Scheme</h2>
|
||||
{isActive ? (
|
||||
<select
|
||||
value={run.namingScheme ?? ''}
|
||||
onChange={(e) => updateRun.mutate({ namingScheme: e.target.value || null })}
|
||||
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"
|
||||
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{namingCategories.map((cat) => (
|
||||
@@ -213,7 +209,7 @@ export function RunDashboard() {
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span className="text-sm text-gray-900 dark:text-gray-100">
|
||||
<span className="text-sm text-text-primary">
|
||||
{run.namingScheme
|
||||
? run.namingScheme.charAt(0).toUpperCase() + run.namingScheme.slice(1)
|
||||
: 'None'}
|
||||
@@ -225,14 +221,14 @@ export function RunDashboard() {
|
||||
{/* Active Team */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{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"
|
||||
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
|
||||
>
|
||||
<option value="route">Route Order</option>
|
||||
<option value="level">Catch Level</option>
|
||||
@@ -242,7 +238,7 @@ export function RunDashboard() {
|
||||
)}
|
||||
</div>
|
||||
{alive.length === 0 ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
||||
<p className="text-text-tertiary text-sm">
|
||||
No pokemon caught yet — head to encounters to start building your team!
|
||||
</p>
|
||||
) : (
|
||||
@@ -261,7 +257,7 @@ export function RunDashboard() {
|
||||
{/* 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>
|
||||
<h2 className="text-lg font-semibold text-text-primary 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
|
||||
@@ -281,13 +277,13 @@ export function RunDashboard() {
|
||||
<>
|
||||
<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"
|
||||
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-accent-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Log Encounter
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setShowEndRun(true)}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg font-medium hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
className="px-4 py-2 border border-border-default rounded-lg font-medium hover:bg-surface-2 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
End Run
|
||||
</button>
|
||||
|
||||
@@ -93,9 +93,9 @@ const statusIndicator: Record<RouteStatus, { dot: string; label: string; bg: str
|
||||
missed: {
|
||||
dot: 'bg-gray-400',
|
||||
label: 'Missed',
|
||||
bg: 'bg-gray-50 dark:bg-gray-900/10',
|
||||
bg: 'bg-surface-0/10',
|
||||
},
|
||||
none: { dot: 'bg-gray-300 dark:bg-gray-600', label: '', bg: '' },
|
||||
none: { dot: 'bg-surface-3', label: '', bg: '' },
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +222,7 @@ function BossTeamPreview({
|
||||
className={`px-2 py-0.5 text-xs font-medium rounded-full transition-colors ${
|
||||
selectedVariant === label
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@@ -238,10 +238,10 @@ function BossTeamPreview({
|
||||
{bp.pokemon.spriteUrl ? (
|
||||
<img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-20 h-20" />
|
||||
) : (
|
||||
<div className="w-20 h-20 bg-gray-200 dark:bg-gray-700 rounded-full" />
|
||||
<div className="w-20 h-20 bg-surface-3 rounded-full" />
|
||||
)}
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Lvl {bp.level}</span>
|
||||
<span className="text-xs text-text-tertiary">Lvl {bp.level}</span>
|
||||
<ConditionBadge condition={bp.conditionLabel} size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -302,20 +302,18 @@ function RouteGroup({
|
||||
const hasGroupEncounter = groupEncounter !== null
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="border border-border-default rounded-lg overflow-hidden">
|
||||
{/* Group header */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleExpand}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-surface-2/50 ${si.bg}`}
|
||||
>
|
||||
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-text-primary flex items-center gap-2">
|
||||
{group.name}
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
({group.children.length} areas)
|
||||
</span>
|
||||
<span className="text-xs text-text-muted">({group.children.length} areas)</span>
|
||||
</div>
|
||||
{groupEncounter && (
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
@@ -326,7 +324,7 @@ function RouteGroup({
|
||||
className="w-10 h-10"
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
|
||||
<span className="text-xs text-text-tertiary capitalize">
|
||||
{groupEncounter.nickname ?? groupEncounter.pokemon.name}
|
||||
{groupEncounter.status === 'caught' &&
|
||||
groupEncounter.faintLevel !== null &&
|
||||
@@ -335,7 +333,7 @@ function RouteGroup({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">{si.label}</span>
|
||||
<span className="text-xs text-text-muted shrink-0">{si.label}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
@@ -348,7 +346,7 @@ function RouteGroup({
|
||||
|
||||
{/* Expanded children */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50">
|
||||
<div className="border-t border-border-default bg-surface-1/50">
|
||||
{group.children.map((child) => {
|
||||
const childEncounter = encounterByRoute.get(child.id)
|
||||
const childStatus = getRouteStatus(childEncounter)
|
||||
@@ -371,14 +369,12 @@ function RouteGroup({
|
||||
onClick={() => !isDisabled && onRouteClick(child)}
|
||||
disabled={isDisabled}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2 pl-8 text-left transition-colors ${
|
||||
isDisabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
|
||||
isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-surface-2/50'
|
||||
} ${childSi.bg}`}
|
||||
>
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${childSi.dot}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">{child.name}</div>
|
||||
<div className="text-sm text-text-secondary">{child.name}</div>
|
||||
{!childEncounter && child.encounterMethods.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-0.5">
|
||||
{child.encounterMethods.map((m) => (
|
||||
@@ -387,12 +383,8 @@ function RouteGroup({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{childEncounter && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{childSi.label}</span>
|
||||
)}
|
||||
{isDisabled && (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 italic">(locked)</span>
|
||||
)}
|
||||
{childEncounter && <span className="text-xs text-text-muted">{childSi.label}</span>}
|
||||
{isDisabled && <span className="text-xs text-text-muted italic">(locked)</span>}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -674,7 +666,7 @@ export function RunEncounters() {
|
||||
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">
|
||||
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to load run.
|
||||
</div>
|
||||
<Link to="/runs" className="inline-block mt-4 text-blue-600 hover:underline">
|
||||
@@ -792,14 +784,14 @@ export function RunEncounters() {
|
||||
<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"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary 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">
|
||||
<h1 className="text-3xl font-bold text-text-primary">{run.name}</h1>
|
||||
<p className="text-text-tertiary mt-1">
|
||||
{run.game.name} ·{' '}
|
||||
{run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} · Started{' '}
|
||||
{new Date(run.startedAt).toLocaleDateString(undefined, {
|
||||
@@ -827,7 +819,7 @@ export function RunEncounters() {
|
||||
{isActive && (
|
||||
<button
|
||||
onClick={() => setShowEggModal(true)}
|
||||
className="px-3 py-1 text-sm border border-green-400 dark:border-green-600 text-green-600 dark:text-green-400 rounded-full font-medium hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
|
||||
className="px-3 py-1 text-sm border border-green-400 dark:border-green-600 text-status-active rounded-full font-medium hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
|
||||
>
|
||||
🥚 Log Egg
|
||||
</button>
|
||||
@@ -835,7 +827,7 @@ export function RunEncounters() {
|
||||
{isActive && (
|
||||
<button
|
||||
onClick={() => setShowEndRun(true)}
|
||||
className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded-full font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
className="px-3 py-1 text-sm border border-border-default rounded-full font-medium hover:bg-surface-2 transition-colors"
|
||||
>
|
||||
End Run
|
||||
</button>
|
||||
@@ -855,7 +847,7 @@ export function RunEncounters() {
|
||||
className={`rounded-lg p-4 mb-6 ${
|
||||
run.status === 'completed'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
|
||||
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
|
||||
: 'bg-status-failed-bg border border-red-200 dark:border-red-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -881,9 +873,7 @@ export function RunEncounters() {
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm ${
|
||||
run.status === 'completed'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
run.status === 'completed' ? 'text-text-link' : 'text-status-failed'
|
||||
}`}
|
||||
>
|
||||
{run.completedAt && (
|
||||
@@ -926,7 +916,7 @@ export function RunEncounters() {
|
||||
}
|
||||
}}
|
||||
disabled={advanceLeg.isPending}
|
||||
className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
className="px-4 py-2 text-sm font-medium rounded-lg bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{advanceLeg.isPending ? 'Advancing...' : 'Advance to Next Leg'}
|
||||
</button>
|
||||
@@ -936,7 +926,7 @@ export function RunEncounters() {
|
||||
{run.status === 'completed' && (
|
||||
<div className="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider">
|
||||
<span className="text-xs font-medium text-text-link uppercase tracking-wider">
|
||||
Hall of Fame
|
||||
</span>
|
||||
<button
|
||||
@@ -956,11 +946,11 @@ export function RunEncounters() {
|
||||
{dp.spriteUrl ? (
|
||||
<img src={dp.spriteUrl} alt={dp.name} className="w-12 h-12" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold">
|
||||
<div className="w-12 h-12 rounded-full bg-surface-3 flex items-center justify-center text-sm font-bold">
|
||||
{dp.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-[10px] text-blue-600 dark:text-blue-400 capitalize mt-0.5">
|
||||
<span className="text-[10px] text-text-link capitalize mt-0.5">
|
||||
{enc.nickname || dp.name}
|
||||
</span>
|
||||
</div>
|
||||
@@ -987,24 +977,20 @@ export function RunEncounters() {
|
||||
|
||||
{/* Level Cap Bar */}
|
||||
{run.rules?.levelCaps && sortedBosses.length > 0 && (
|
||||
<div className="sticky top-0 z-10 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 mb-6 shadow-sm">
|
||||
<div className="sticky top-0 z-10 bg-surface-0 border border-border-default rounded-lg px-4 py-3 mb-6 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<span className="text-sm font-semibold text-text-primary">
|
||||
Level Cap: {currentLevelCap ?? '—'}
|
||||
</span>
|
||||
{nextBoss && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Next: {nextBoss.name}
|
||||
</span>
|
||||
<span className="text-sm text-text-tertiary">Next: {nextBoss.name}</span>
|
||||
)}
|
||||
{!nextBoss && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">
|
||||
All bosses defeated!
|
||||
</span>
|
||||
<span className="text-sm text-status-active">All bosses defeated!</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
<span className="text-xs text-text-muted">
|
||||
{defeatedBossIds.size}/{sortedBosses.length} defeated
|
||||
</span>
|
||||
</div>
|
||||
@@ -1032,7 +1018,7 @@ export function RunEncounters() {
|
||||
className={`w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
|
||||
earned
|
||||
? 'border-yellow-500 bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300'
|
||||
: 'border-gray-300 dark:border-gray-600 text-gray-400'
|
||||
: 'border-border-default text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{boss.order}
|
||||
@@ -1048,7 +1034,7 @@ export function RunEncounters() {
|
||||
|
||||
{/* Rules */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Active Rules</h2>
|
||||
<h2 className="text-sm font-medium text-text-tertiary mb-2">Active Rules</h2>
|
||||
<RuleBadges rules={run.rules} />
|
||||
</div>
|
||||
|
||||
@@ -1061,10 +1047,10 @@ export function RunEncounters() {
|
||||
onClick={() => setShowTeam(!showTeam)}
|
||||
className="flex items-center gap-2 group"
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{isActive ? 'Team' : 'Final Team'}
|
||||
</h2>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
<span className="text-xs text-text-muted">
|
||||
{alive.length} alive
|
||||
{dead.length > 0 ? `, ${dead.length} dead` : ''}
|
||||
</span>
|
||||
@@ -1086,7 +1072,7 @@ export function RunEncounters() {
|
||||
<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"
|
||||
className="text-sm border border-border-default rounded-lg px-3 py-1.5 bg-surface-1 text-text-primary"
|
||||
>
|
||||
<option value="route">Route Order</option>
|
||||
<option value="level">Catch Level</option>
|
||||
@@ -1110,9 +1096,7 @@ export function RunEncounters() {
|
||||
)}
|
||||
{dead.length > 0 && (
|
||||
<>
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
Graveyard
|
||||
</h3>
|
||||
<h3 className="text-sm font-medium text-text-tertiary mb-2">Graveyard</h3>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
|
||||
{dead.map((enc) => (
|
||||
<PokemonCard
|
||||
@@ -1162,7 +1146,7 @@ export function RunEncounters() {
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Encounters</h2>
|
||||
<h2 className="text-lg font-semibold text-text-primary">Encounters</h2>
|
||||
{isActive && completedCount < totalLocations && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -1181,11 +1165,11 @@ export function RunEncounters() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm text-text-tertiary">
|
||||
{completedCount} / {totalLocations} locations
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className="h-2 bg-surface-3 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full transition-all"
|
||||
style={{
|
||||
@@ -1212,7 +1196,7 @@ export function RunEncounters() {
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
|
||||
filter === key
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||
: 'bg-surface-2 text-text-secondary hover:bg-surface-3'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@@ -1223,7 +1207,7 @@ export function RunEncounters() {
|
||||
{/* Route list */}
|
||||
<div className="space-y-1">
|
||||
{filteredRoutes.length === 0 && (
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm py-4 text-center">
|
||||
<p className="text-text-tertiary text-sm py-4 text-center">
|
||||
{filter === 'all'
|
||||
? 'Click a route to log your first encounter'
|
||||
: 'No routes match this filter — try a different one'}
|
||||
@@ -1264,13 +1248,11 @@ export function RunEncounters() {
|
||||
key={route.id}
|
||||
type="button"
|
||||
onClick={() => handleRouteClick(route)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-surface-2/50 ${si.bg}`}
|
||||
>
|
||||
<span className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{route.name}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-text-primary">{route.name}</div>
|
||||
{encounter ? (
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
{encounter.pokemon.spriteUrl && (
|
||||
@@ -1280,7 +1262,7 @@ export function RunEncounters() {
|
||||
className="w-10 h-10"
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
|
||||
<span className="text-xs text-text-tertiary capitalize">
|
||||
{encounter.nickname ?? encounter.pokemon.name}
|
||||
{encounter.status === 'caught' &&
|
||||
encounter.faintLevel !== null &&
|
||||
@@ -1297,9 +1279,7 @@ export function RunEncounters() {
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
|
||||
{si.label}
|
||||
</span>
|
||||
<span className="text-xs text-text-muted shrink-0">{si.label}</span>
|
||||
</button>
|
||||
)
|
||||
})()
|
||||
@@ -1347,9 +1327,7 @@ export function RunEncounters() {
|
||||
<div key={`boss-${boss.id}`}>
|
||||
<div
|
||||
className={`my-2 rounded-lg border-2 ${bossTypeColors[boss.bossType] ?? bossTypeColors['other']} ${
|
||||
isDefeated
|
||||
? 'bg-green-50/50 dark:bg-green-900/10'
|
||||
: 'bg-white dark:bg-gray-800'
|
||||
isDefeated ? 'bg-green-50/50 dark:bg-green-900/10' : 'bg-surface-1'
|
||||
} px-4 py-3`}
|
||||
>
|
||||
<div
|
||||
@@ -1371,15 +1349,15 @@ export function RunEncounters() {
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<span className="text-sm font-semibold text-text-primary">
|
||||
{boss.name}
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
|
||||
<span className="px-2 py-0.5 text-xs font-medium rounded-full bg-surface-2 text-text-secondary">
|
||||
{bossTypeLabel[boss.bossType] ?? boss.bossType}
|
||||
</span>
|
||||
{boss.specialtyType && <TypeBadge type={boss.specialtyType} />}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{boss.location} · Level Cap: {boss.levelCap}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1392,7 +1370,7 @@ export function RunEncounters() {
|
||||
) : isActive ? (
|
||||
<button
|
||||
onClick={() => setSelectedBoss(boss)}
|
||||
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
|
||||
className="px-3 py-1 text-xs font-medium rounded-full bg-accent-600 text-white hover:bg-accent-500 transition-colors"
|
||||
>
|
||||
Battle
|
||||
</button>
|
||||
@@ -1406,11 +1384,11 @@ export function RunEncounters() {
|
||||
</div>
|
||||
{sectionAfter && (
|
||||
<div className="flex items-center gap-3 my-4">
|
||||
<div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
|
||||
<span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<div className="flex-1 h-px bg-surface-3" />
|
||||
<span className="text-sm font-semibold text-text-tertiary uppercase tracking-wider">
|
||||
{sectionAfter}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
|
||||
<div className="flex-1 h-px bg-surface-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useRuns } from '../hooks/useRuns'
|
||||
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',
|
||||
active: 'bg-status-active-bg text-status-active border border-status-active/20',
|
||||
completed: 'bg-status-completed-bg text-status-completed border border-status-completed/20',
|
||||
failed: 'bg-status-failed-bg text-status-failed border border-status-failed/20',
|
||||
}
|
||||
|
||||
export function RunList() {
|
||||
@@ -14,10 +14,10 @@ export function RunList() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Your Runs</h1>
|
||||
<h1 className="text-3xl font-bold text-text-primary">Your Runs</h1>
|
||||
<Link
|
||||
to="/runs/new"
|
||||
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"
|
||||
className="px-4 py-2 bg-accent-600 text-white rounded-lg font-medium hover:bg-accent-500 focus:outline-none focus:ring-2 focus:ring-accent-400 focus:ring-offset-2 focus:ring-offset-surface-0 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Start New Run
|
||||
</Link>
|
||||
@@ -25,24 +25,24 @@ export function RunList() {
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-4 border-accent-400 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
<div className="rounded-lg bg-status-failed-bg border border-status-failed/20 p-4 text-status-failed">
|
||||
Failed to load runs. Please try again.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{runs && runs.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 mb-4">
|
||||
<p className="text-lg text-text-secondary mb-4">
|
||||
No runs yet. Start your first Nuzlocke!
|
||||
</p>
|
||||
<Link
|
||||
to="/runs/new"
|
||||
className="inline-block px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||
className="inline-block px-6 py-2 bg-accent-600 text-white rounded-lg font-medium hover:bg-accent-500 transition-all active:scale-[0.98]"
|
||||
>
|
||||
Start New Run
|
||||
</Link>
|
||||
@@ -50,19 +50,17 @@ export function RunList() {
|
||||
)}
|
||||
|
||||
{runs && runs.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
{runs.map((run) => (
|
||||
<Link
|
||||
key={run.id}
|
||||
to={`/runs/${run.id}`}
|
||||
className="block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow p-4"
|
||||
className="block bg-surface-1 rounded-xl border border-border-default hover:border-border-accent transition-all hover:-translate-y-0.5 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{run.name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-lg font-semibold text-text-primary">{run.name}</h2>
|
||||
<p className="text-sm text-text-secondary">
|
||||
Started{' '}
|
||||
{new Date(run.startedAt).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
|
||||
@@ -40,24 +40,22 @@ function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRankin
|
||||
|
||||
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-text-secondary mb-2">{title}</h3>
|
||||
{pokemon.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No data</p>
|
||||
<p className="text-sm text-text-tertiary">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>
|
||||
<span className="text-text-muted w-5 text-right">{i + 1}.</span>
|
||||
{p.spriteUrl ? (
|
||||
<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" />
|
||||
<div className="w-6 h-6 bg-surface-3 rounded" />
|
||||
)}
|
||||
<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>
|
||||
<span className="capitalize text-text-primary">{p.name}</span>
|
||||
<span className="ml-auto text-text-tertiary font-medium">{p.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -65,7 +63,7 @@ function PokemonList({ title, pokemon }: { title: string; pokemon: PokemonRankin
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
||||
className="mt-2 text-xs text-text-link hover:underline"
|
||||
>
|
||||
{expanded ? 'Show less' : `Show all ${pokemon.length}`}
|
||||
</button>
|
||||
@@ -100,7 +98,7 @@ function HorizontalBar({
|
||||
const isLight = colorHex ? hexLuminance(colorHex) > 0.55 : false
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex-1 bg-gray-100 dark:bg-gray-700 rounded-full h-6 overflow-hidden relative">
|
||||
<div className="flex-1 bg-surface-2 rounded-full h-6 overflow-hidden relative">
|
||||
<div
|
||||
className={`h-full rounded-full ${color ?? ''}`}
|
||||
style={{
|
||||
@@ -110,7 +108,7 @@ 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-text-primary'
|
||||
}`}
|
||||
style={{
|
||||
textShadow: isLight ? '0 0 4px rgba(255,255,255,0.8)' : '0 0 4px rgba(0,0,0,0.3)',
|
||||
@@ -119,17 +117,15 @@ function HorizontalBar({
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<span className="w-8 text-right text-gray-700 dark:text-gray-300 font-medium shrink-0">
|
||||
{value}
|
||||
</span>
|
||||
<span className="w-8 text-right text-text-secondary font-medium shrink-0">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
<section className="bg-surface-1 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-bold text-text-primary mb-4">{title}</h2>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
@@ -149,16 +145,13 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
<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">
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-text-tertiary">
|
||||
<span>
|
||||
Win Rate:{' '}
|
||||
<strong className="text-gray-800 dark:text-gray-200">{pct(stats.winRate)}</strong>
|
||||
Win Rate: <strong className="text-text-primary">{pct(stats.winRate)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
Avg Duration:{' '}
|
||||
<strong className="text-gray-800 dark:text-gray-200">
|
||||
{fmt(stats.avgDurationDays, ' days')}
|
||||
</strong>
|
||||
<strong className="text-text-primary">{fmt(stats.avgDurationDays, ' days')}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</Section>
|
||||
@@ -202,16 +195,13 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
color="gray"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm text-text-tertiary">
|
||||
<span>
|
||||
Catch Rate:{' '}
|
||||
<strong className="text-gray-800 dark:text-gray-200">{pct(stats.catchRate)}</strong>
|
||||
Catch Rate: <strong className="text-text-primary">{pct(stats.catchRate)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
Avg per Run:{' '}
|
||||
<strong className="text-gray-800 dark:text-gray-200">
|
||||
{fmt(stats.avgEncountersPerRun)}
|
||||
</strong>
|
||||
<strong className="text-text-primary">{fmt(stats.avgEncountersPerRun)}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</Section>
|
||||
@@ -228,39 +218,29 @@ function StatsContent({ stats }: { stats: StatsResponse }) {
|
||||
<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" />
|
||||
<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="bg-surface-1 rounded-lg shadow p-4 border-l-4 border-amber-500">
|
||||
<div className="text-2xl font-bold text-text-primary">{pct(stats.mortalityRate)}</div>
|
||||
<div className="text-sm text-text-tertiary">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="bg-surface-1 rounded-lg shadow p-4 border-l-4 border-blue-500">
|
||||
<div className="text-2xl font-bold text-text-primary">{fmt(stats.avgCatchLevel)}</div>
|
||||
<div className="text-sm text-text-tertiary">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="bg-surface-1 rounded-lg shadow p-4 border-l-4 border-purple-500">
|
||||
<div className="text-2xl font-bold text-text-primary">{fmt(stats.avgFaintLevel)}</div>
|
||||
<div className="text-sm text-text-tertiary">Avg Faint Lv.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats.topDeathCauses.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
Top Death Causes
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold text-text-secondary mb-2">Top Death Causes</h3>
|
||||
<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="ml-auto text-gray-500 dark:text-gray-400 font-medium">
|
||||
{d.count}
|
||||
</span>
|
||||
<span className="text-text-muted w-5 text-right">{i + 1}.</span>
|
||||
<span className="text-text-primary">{d.cause}</span>
|
||||
<span className="ml-auto text-text-tertiary font-medium">{d.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -293,7 +273,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-text-primary mb-6">Stats</h1>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center py-12">
|
||||
@@ -302,13 +282,13 @@ export function Stats() {
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
|
||||
<div className="rounded-lg bg-status-failed-bg p-4 text-status-failed">
|
||||
Failed to load stats: {error.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats && stats.totalRuns === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<div className="text-center py-12 text-text-tertiary">
|
||||
<p className="text-lg mb-2">No data yet</p>
|
||||
<p className="text-sm">Start a Nuzlocke run to see your stats here.</p>
|
||||
</div>
|
||||
|
||||
@@ -83,19 +83,19 @@ export function AdminEvolutions() {
|
||||
const data = await exportEvolutions()
|
||||
downloadJson(data, 'evolutions.json')
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowBulkImport(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Bulk Import
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Evolution
|
||||
</button>
|
||||
@@ -111,7 +111,7 @@ export function AdminEvolutions() {
|
||||
setPage(0)
|
||||
}}
|
||||
placeholder="Search by pokemon name, trigger, or item..."
|
||||
className="w-full max-w-sm px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
/>
|
||||
<select
|
||||
value={triggerFilter}
|
||||
@@ -119,7 +119,7 @@ export function AdminEvolutions() {
|
||||
setTriggerFilter(e.target.value)
|
||||
setPage(0)
|
||||
}}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All triggers</option>
|
||||
{EVOLUTION_TRIGGERS.map((t) => (
|
||||
@@ -135,14 +135,12 @@ export function AdminEvolutions() {
|
||||
setTriggerFilter('')
|
||||
setPage(0)
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{total} evolutions
|
||||
</span>
|
||||
<span className="text-sm text-text-tertiary whitespace-nowrap">{total} evolutions</span>
|
||||
</div>
|
||||
|
||||
<AdminTable
|
||||
@@ -156,38 +154,38 @@ export function AdminEvolutions() {
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="text-sm text-text-tertiary">
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(0)}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
First
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 px-2">
|
||||
<span className="text-sm text-text-secondary px-2">
|
||||
Page {page + 1} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(totalPages - 1)}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
|
||||
@@ -95,18 +95,15 @@ function SortableRouteGroup({
|
||||
<tbody
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} divide-y divide-gray-200 dark:divide-gray-700`}
|
||||
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} divide-y divide-border-default`}
|
||||
>
|
||||
<tr
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
onClick={() => onClick(group)}
|
||||
>
|
||||
<tr className="hover:bg-surface-2 cursor-pointer" onClick={() => onClick(group)}>
|
||||
<td className="px-4 py-3 text-sm w-12">
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 touch-none"
|
||||
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-text-secondary touch-none"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
@@ -128,7 +125,7 @@ function SortableRouteGroup({
|
||||
<Link
|
||||
to={`/admin/games/${gameId}/routes/${group.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
className="text-text-link hover:underline"
|
||||
>
|
||||
Encounters
|
||||
</Link>
|
||||
@@ -137,15 +134,15 @@ function SortableRouteGroup({
|
||||
{group.children.map((child) => (
|
||||
<tr
|
||||
key={child.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="hover:bg-surface-2 cursor-pointer"
|
||||
onClick={() => onClick(child)}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm w-12" />
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap w-16 text-gray-400 dark:text-gray-500">
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap w-16 text-text-muted">
|
||||
{child.order}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap pl-8 text-gray-600 dark:text-gray-400">
|
||||
<span className="text-gray-300 dark:text-gray-600 mr-1.5">{'\u2514'}</span>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap pl-8 text-text-tertiary">
|
||||
<span className="text-text-muted mr-1.5">{'\u2514'}</span>
|
||||
{child.name}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap text-center">
|
||||
@@ -155,7 +152,7 @@ function SortableRouteGroup({
|
||||
<Link
|
||||
to={`/admin/games/${gameId}/routes/${child.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
className="text-text-link hover:underline"
|
||||
>
|
||||
Encounters
|
||||
</Link>
|
||||
@@ -192,7 +189,7 @@ function SortableBossRow({
|
||||
<tr
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer`}
|
||||
className={`${isDragging ? 'opacity-50 bg-blue-50 dark:bg-blue-900/20' : ''} hover:bg-surface-2 cursor-pointer`}
|
||||
onClick={() => onClick(boss)}
|
||||
>
|
||||
<td className="px-4 py-3 text-sm w-12">
|
||||
@@ -200,7 +197,7 @@ function SortableBossRow({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 touch-none"
|
||||
className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-text-secondary touch-none"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
@@ -220,7 +217,7 @@ function SortableBossRow({
|
||||
(() => {
|
||||
const g = games.find((g) => g.id === boss.gameId)
|
||||
return g ? (
|
||||
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
|
||||
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-surface-2 text-text-tertiary">
|
||||
{g.name}
|
||||
</span>
|
||||
) : null
|
||||
@@ -242,7 +239,7 @@ function SortableBossRow({
|
||||
const value = e.target.value === '' ? null : Number(e.target.value)
|
||||
onPositionChange(boss.id, value)
|
||||
}}
|
||||
className="text-sm border border-gray-300 dark:border-gray-600 rounded px-1.5 py-0.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 max-w-[180px]"
|
||||
className="text-sm border border-border-default rounded px-1.5 py-0.5 bg-surface-1 text-text-primary max-w-[180px]"
|
||||
>
|
||||
<option value="">—</option>
|
||||
{routes.map((r) => (
|
||||
@@ -343,30 +340,30 @@ export function AdminGameDetail() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
|
||||
<nav className="text-sm mb-4 text-text-tertiary">
|
||||
<Link to="/admin/games" className="hover:underline">
|
||||
Games
|
||||
</Link>
|
||||
{' / '}
|
||||
<span className="text-gray-900 dark:text-gray-100">{game.name}</span>
|
||||
<span className="text-text-primary">{game.name}</span>
|
||||
</nav>
|
||||
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold">{game.name}</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-text-tertiary">
|
||||
{game.region.charAt(0).toUpperCase() + game.region.slice(1)} · Gen{' '}
|
||||
{game.generation}
|
||||
{game.releaseYear ? ` \u00b7 ${game.releaseYear}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700 mb-4">
|
||||
<div className="flex border-b border-border-default mb-4">
|
||||
<button
|
||||
onClick={() => setTab('routes')}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||
tab === 'routes'
|
||||
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
: 'border-transparent text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
Routes ({routes.length})
|
||||
@@ -376,7 +373,7 @@ export function AdminGameDetail() {
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px ${
|
||||
tab === 'bosses'
|
||||
? 'border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
: 'border-transparent text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
Boss Battles ({bosses?.length ?? 0})
|
||||
@@ -391,19 +388,19 @@ export function AdminGameDetail() {
|
||||
const result = await exportGameRoutes(id)
|
||||
downloadJson(result.data, result.filename)
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowBulkImportRoutes(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Bulk Import
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Route
|
||||
</button>
|
||||
@@ -421,26 +418,26 @@ export function AdminGameDetail() {
|
||||
)}
|
||||
|
||||
{routes.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
|
||||
No routes yet. Add one to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="border border-border-default rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<table className="min-w-full divide-y divide-border-default">
|
||||
<thead className="bg-surface-1">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-12" />
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-12" />
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
|
||||
Order
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-text-tertiary uppercase tracking-wider w-24">
|
||||
Pinwheel
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-28">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-28">
|
||||
Links
|
||||
</th>
|
||||
</tr>
|
||||
@@ -513,19 +510,19 @@ export function AdminGameDetail() {
|
||||
const result = await exportGameBosses(id)
|
||||
downloadJson(result.data, result.filename)
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowBulkImportBosses(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Bulk Import
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateBoss(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Boss Battle
|
||||
</button>
|
||||
@@ -543,41 +540,41 @@ export function AdminGameDetail() {
|
||||
)}
|
||||
|
||||
{!bosses || bosses.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
|
||||
No boss battles yet. Add one to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="border border-border-default rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<table className="min-w-full divide-y divide-border-default">
|
||||
<thead className="bg-surface-1">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-12" />
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-12" />
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
|
||||
Order
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Specialty
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Section
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Location
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Position
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
|
||||
Lv Cap
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
|
||||
Team
|
||||
</th>
|
||||
</tr>
|
||||
@@ -591,7 +588,7 @@ export function AdminGameDetail() {
|
||||
items={bosses.map((b) => b.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody className="bg-surface-0 divide-y divide-border-default">
|
||||
{bosses.map((boss) => (
|
||||
<SortableBossRow
|
||||
key={boss.id}
|
||||
|
||||
@@ -57,13 +57,13 @@ export function AdminGames() {
|
||||
const data = await exportGames()
|
||||
downloadJson(data, 'games.json')
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Game
|
||||
</button>
|
||||
@@ -74,7 +74,7 @@ export function AdminGames() {
|
||||
<select
|
||||
value={regionFilter}
|
||||
onChange={(e) => setRegionFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All regions</option>
|
||||
{regions.map((r) => (
|
||||
@@ -86,7 +86,7 @@ export function AdminGames() {
|
||||
<select
|
||||
value={genFilter}
|
||||
onChange={(e) => setGenFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All generations</option>
|
||||
{generations.map((g) => (
|
||||
@@ -101,12 +101,12 @@ export function AdminGames() {
|
||||
setRegionFilter('')
|
||||
setGenFilter('')
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
<span className="text-sm text-text-tertiary whitespace-nowrap">
|
||||
{filteredGames.length} games
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -67,36 +67,32 @@ export function AdminGenlockeDetail() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
|
||||
<nav className="text-sm mb-4 text-text-tertiary">
|
||||
<Link to="/admin/genlockes" className="hover:underline">
|
||||
Genlockes
|
||||
</Link>
|
||||
{' / '}
|
||||
<span className="text-gray-900 dark:text-gray-100">{genlocke.name}</span>
|
||||
<span className="text-text-primary">{genlocke.name}</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-6 space-y-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Name
|
||||
</label>
|
||||
<label className="block text-xs font-medium text-text-tertiary mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="w-full px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
Status
|
||||
</label>
|
||||
<label className="block text-xs font-medium text-text-tertiary mb-1">Status</label>
|
||||
<select
|
||||
value={editStatus}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="active">Active</option>
|
||||
<option value="completed">Completed</option>
|
||||
@@ -106,35 +102,35 @@ export function AdminGenlockeDetail() {
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || updateGenlocke.isPending}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500 disabled:opacity-50"
|
||||
>
|
||||
{updateGenlocke.isPending ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDelete(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-red-300 dark:border-red-600 text-status-failed hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-text-tertiary">
|
||||
Created {new Date(genlocke.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Rules (read-only) */}
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Rules</h3>
|
||||
<div className="mb-6 p-4 bg-surface-1 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 text-text-secondary">Rules</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Genlocke rules:</span>
|
||||
<pre className="mt-1 text-xs bg-white dark:bg-gray-900 p-2 rounded border dark:border-gray-700 overflow-x-auto">
|
||||
<span className="text-text-tertiary">Genlocke rules:</span>
|
||||
<pre className="mt-1 text-xs bg-surface-0 p-2 rounded border border-border-default overflow-x-auto">
|
||||
{JSON.stringify(genlocke.genlockeRules, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Nuzlocke rules:</span>
|
||||
<pre className="mt-1 text-xs bg-white dark:bg-gray-900 p-2 rounded border dark:border-gray-700 overflow-x-auto">
|
||||
<span className="text-text-tertiary">Nuzlocke rules:</span>
|
||||
<pre className="mt-1 text-xs bg-surface-0 p-2 rounded border border-border-default overflow-x-auto">
|
||||
{JSON.stringify(genlocke.nuzlockeRules, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -147,18 +143,18 @@ export function AdminGenlockeDetail() {
|
||||
<h3 className="text-lg font-semibold">Legs ({genlocke.legs.length})</h3>
|
||||
<button
|
||||
onClick={() => setAddingLeg(!addingLeg)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Leg
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{addingLeg && (
|
||||
<div className="mb-4 flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<div className="mb-4 flex items-center gap-3 p-3 bg-surface-1 rounded-lg">
|
||||
<select
|
||||
value={selectedGameId}
|
||||
onChange={(e) => setSelectedGameId(e.target.value ? Number(e.target.value) : '')}
|
||||
className="flex-1 px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="flex-1 px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">Select a game...</option>
|
||||
{games.map((g) => (
|
||||
@@ -179,7 +175,7 @@ export function AdminGenlockeDetail() {
|
||||
setAddingLeg(false)
|
||||
setSelectedGameId('')
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -187,39 +183,39 @@ export function AdminGenlockeDetail() {
|
||||
)}
|
||||
|
||||
{genlocke.legs.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="text-center py-8 text-text-tertiary border border-border-default rounded-lg">
|
||||
No legs yet. Add one to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
|
||||
<div className="border border-border-default rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
<table className="min-w-full divide-y divide-border-default">
|
||||
<thead className="bg-surface-1">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-16">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-16">
|
||||
Order
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Game
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Run
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
|
||||
Enc.
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
|
||||
Deaths
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-20">
|
||||
<th className="px-4 py-3 text-right text-xs font-medium text-text-tertiary uppercase tracking-wider w-20">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody className="bg-surface-0 divide-y divide-border-default">
|
||||
{genlocke.legs.map((leg) => (
|
||||
<tr key={leg.id}>
|
||||
<td className="px-4 py-3 text-sm whitespace-nowrap">{leg.legOrder}</td>
|
||||
@@ -228,7 +224,7 @@ export function AdminGenlockeDetail() {
|
||||
{leg.runId ? (
|
||||
<Link
|
||||
to={`/runs/${leg.runId}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
className="text-text-link hover:underline"
|
||||
>
|
||||
Run #{leg.runId}
|
||||
</Link>
|
||||
@@ -241,10 +237,10 @@ export function AdminGenlockeDetail() {
|
||||
<span
|
||||
className={
|
||||
leg.runStatus === 'active'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
? 'text-status-active'
|
||||
: leg.runStatus === 'completed'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
? 'text-text-link'
|
||||
: 'text-status-failed'
|
||||
}
|
||||
>
|
||||
{leg.runStatus}
|
||||
@@ -264,7 +260,7 @@ export function AdminGenlockeDetail() {
|
||||
? 'Cannot remove a leg with a linked run'
|
||||
: 'Remove leg'
|
||||
}
|
||||
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
className="text-status-failed hover:text-red-800 dark:hover:text-red-300 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@@ -279,25 +275,25 @@ export function AdminGenlockeDetail() {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Stats</h3>
|
||||
<div className="mb-6 p-4 bg-surface-1 rounded-lg">
|
||||
<h3 className="text-sm font-semibold mb-2 text-text-secondary">Stats</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Legs</span>
|
||||
<span className="text-text-tertiary">Legs</span>
|
||||
<p className="text-lg font-semibold">
|
||||
{genlocke.stats.legsCompleted} / {genlocke.stats.totalLegs}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Encounters</span>
|
||||
<span className="text-text-tertiary">Encounters</span>
|
||||
<p className="text-lg font-semibold">{genlocke.stats.totalEncounters}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Deaths</span>
|
||||
<span className="text-text-tertiary">Deaths</span>
|
||||
<p className="text-lg font-semibold">{genlocke.stats.totalDeaths}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500 dark:text-gray-400">Survival Rate</span>
|
||||
<span className="text-text-tertiary">Survival Rate</span>
|
||||
<p className="text-lg font-semibold">
|
||||
{genlocke.stats.totalEncounters > 0
|
||||
? `${Math.round(((genlocke.stats.totalEncounters - genlocke.stats.totalDeaths) / genlocke.stats.totalEncounters) * 100)}%`
|
||||
|
||||
@@ -27,10 +27,10 @@ export function AdminGenlockes() {
|
||||
<span
|
||||
className={
|
||||
g.status === 'active'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
? 'text-status-active'
|
||||
: g.status === 'completed'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
? 'text-text-link'
|
||||
: 'text-status-failed'
|
||||
}
|
||||
>
|
||||
{g.status}
|
||||
@@ -60,7 +60,7 @@ export function AdminGenlockes() {
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All statuses</option>
|
||||
<option value="active">Active</option>
|
||||
@@ -70,12 +70,12 @@ export function AdminGenlockes() {
|
||||
{statusFilter && (
|
||||
<button
|
||||
onClick={() => setStatusFilter('')}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
<span className="text-sm text-text-tertiary whitespace-nowrap">
|
||||
{filtered.length} genlockes
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ export function AdminPokemon() {
|
||||
p.spriteUrl ? (
|
||||
<img src={p.spriteUrl} alt={p.name} className="w-8 h-8" />
|
||||
) : (
|
||||
<div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
<div className="w-8 h-8 bg-surface-3 rounded" />
|
||||
),
|
||||
},
|
||||
{ header: 'Name', accessor: (p) => p.name },
|
||||
@@ -85,19 +85,19 @@ export function AdminPokemon() {
|
||||
const data = await exportPokemon()
|
||||
downloadJson(data, 'pokemon.json')
|
||||
}}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default text-text-secondary hover:bg-surface-2"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowBulkImport(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md border border-border-default hover:bg-surface-2"
|
||||
>
|
||||
Bulk Import
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Pokemon
|
||||
</button>
|
||||
@@ -113,7 +113,7 @@ export function AdminPokemon() {
|
||||
setPage(0)
|
||||
}}
|
||||
placeholder="Search by name..."
|
||||
className="w-full max-w-sm px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="w-full max-w-sm px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
/>
|
||||
<select
|
||||
value={typeFilter}
|
||||
@@ -121,7 +121,7 @@ export function AdminPokemon() {
|
||||
setTypeFilter(e.target.value)
|
||||
setPage(0)
|
||||
}}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All types</option>
|
||||
{POKEMON_TYPES.map((t) => (
|
||||
@@ -137,14 +137,12 @@ export function AdminPokemon() {
|
||||
setTypeFilter('')
|
||||
setPage(0)
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
{total} pokemon
|
||||
</span>
|
||||
<span className="text-sm text-text-tertiary whitespace-nowrap">{total} pokemon</span>
|
||||
</div>
|
||||
|
||||
<AdminTable
|
||||
@@ -159,38 +157,38 @@ export function AdminPokemon() {
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="text-sm text-text-tertiary">
|
||||
Showing {offset + 1}-{Math.min(offset + PAGE_SIZE, total)} of {total}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage(0)}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
First
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 px-2">
|
||||
<span className="text-sm text-text-secondary px-2">
|
||||
Page {page + 1} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(totalPages - 1)}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
className="px-3 py-1 text-sm rounded border border-border-default disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-2"
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
|
||||
@@ -98,7 +98,7 @@ export function AdminRouteDetail() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="text-sm mb-4 text-gray-500 dark:text-gray-400">
|
||||
<nav className="text-sm mb-4 text-text-tertiary">
|
||||
<Link to="/admin/games" className="hover:underline">
|
||||
Games
|
||||
</Link>
|
||||
@@ -108,7 +108,7 @@ export function AdminRouteDetail() {
|
||||
</Link>
|
||||
{' / '}
|
||||
<select
|
||||
className="text-gray-900 dark:text-gray-100 bg-transparent font-medium cursor-pointer hover:underline border-none p-0 text-sm"
|
||||
className="text-text-primary bg-transparent font-medium cursor-pointer hover:underline border-none p-0 text-sm"
|
||||
value={rId}
|
||||
onChange={(e) => navigate(`/admin/games/${gId}/routes/${e.target.value}`)}
|
||||
>
|
||||
@@ -129,26 +129,26 @@ export function AdminRouteDetail() {
|
||||
{prevRoute ? (
|
||||
<Link
|
||||
to={`/admin/games/${gId}/routes/${prevRoute.id}`}
|
||||
className="px-2 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
className="px-2 py-1 text-sm rounded border border-border-default hover:bg-surface-2"
|
||||
title={prevRoute.name}
|
||||
>
|
||||
← Prev
|
||||
</Link>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-sm rounded border border-gray-200 dark:border-gray-700 text-gray-300 dark:text-gray-600 cursor-not-allowed">
|
||||
<span className="px-2 py-1 text-sm rounded border border-border-default text-text-muted cursor-not-allowed">
|
||||
← Prev
|
||||
</span>
|
||||
)}
|
||||
{nextRoute ? (
|
||||
<Link
|
||||
to={`/admin/games/${gId}/routes/${nextRoute.id}`}
|
||||
className="px-2 py-1 text-sm rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
className="px-2 py-1 text-sm rounded border border-border-default hover:bg-surface-2"
|
||||
title={nextRoute.name}
|
||||
>
|
||||
Next →
|
||||
</Link>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-sm rounded border border-gray-200 dark:border-gray-700 text-gray-300 dark:text-gray-600 cursor-not-allowed">
|
||||
<span className="px-2 py-1 text-sm rounded border border-border-default text-text-muted cursor-not-allowed">
|
||||
Next →
|
||||
</span>
|
||||
)}
|
||||
@@ -156,7 +156,7 @@ export function AdminRouteDetail() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Pokemon
|
||||
</button>
|
||||
@@ -212,26 +212,26 @@ export function AdminRouteDetail() {
|
||||
<h3 className="text-lg font-semibold">Sub-areas ({childRoutes.length})</h3>
|
||||
<button
|
||||
onClick={() => setShowCreateChild(true)}
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700"
|
||||
className="px-3 py-1.5 text-sm font-medium rounded-md bg-accent-600 text-white hover:bg-accent-500"
|
||||
>
|
||||
Add Sub-area
|
||||
</button>
|
||||
</div>
|
||||
{childRoutes.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">No sub-areas for this route.</p>
|
||||
<p className="text-sm text-text-tertiary">No sub-areas for this route.</p>
|
||||
) : (
|
||||
<div className="border rounded-md dark:border-gray-700 divide-y dark:divide-gray-700">
|
||||
<div className="border border-border-default rounded-md divide-y divide-border-default">
|
||||
{childRoutes.map((child) => (
|
||||
<div key={child.id} className="flex items-center justify-between px-4 py-2">
|
||||
<Link
|
||||
to={`/admin/games/${gId}/routes/${child.id}`}
|
||||
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||
className="text-text-link hover:underline"
|
||||
>
|
||||
{child.name}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setDeletingChild(child)}
|
||||
className="text-sm text-red-600 dark:text-red-400 hover:underline"
|
||||
className="text-sm text-status-failed hover:underline"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
@@ -46,10 +46,10 @@ export function AdminRuns() {
|
||||
<span
|
||||
className={
|
||||
r.status === 'active'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
? 'text-status-active'
|
||||
: r.status === 'completed'
|
||||
? 'text-blue-600 dark:text-blue-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
? 'text-text-link'
|
||||
: 'text-status-failed'
|
||||
}
|
||||
>
|
||||
{r.status}
|
||||
@@ -74,7 +74,7 @@ export function AdminRuns() {
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All statuses</option>
|
||||
<option value="active">Active</option>
|
||||
@@ -84,7 +84,7 @@ export function AdminRuns() {
|
||||
<select
|
||||
value={gameFilter}
|
||||
onChange={(e) => setGameFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md dark:bg-gray-700 dark:border-gray-600"
|
||||
className="px-3 py-2 border rounded-md bg-surface-2 border-border-default"
|
||||
>
|
||||
<option value="">All games</option>
|
||||
{runGames.map(([id, name]) => (
|
||||
@@ -99,12 +99,12 @@ export function AdminRuns() {
|
||||
setStatusFilter('')
|
||||
setGameFilter('')
|
||||
}}
|
||||
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
className="text-sm text-text-tertiary hover:text-text-primary"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||
<span className="text-sm text-text-tertiary whitespace-nowrap">
|
||||
{filteredRuns.length} runs
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user