diff --git a/.beans/nuzlocke-tracker-uw2j--game-selection-screen.md b/.beans/nuzlocke-tracker-uw2j--game-selection-screen.md index e897226..a11e266 100644 --- a/.beans/nuzlocke-tracker-uw2j--game-selection-screen.md +++ b/.beans/nuzlocke-tracker-uw2j--game-selection-screen.md @@ -1,24 +1,30 @@ --- # nuzlocke-tracker-uw2j title: Game Selection Screen -status: todo +status: completed type: task +priority: normal created_at: 2026-02-04T15:44:16Z -updated_at: 2026-02-04T15:44:16Z +updated_at: 2026-02-05T14:02:43Z parent: nuzlocke-tracker-f5ob --- Build the initial screen where users select which Pokémon game they want to track. ## Checklist -- [ ] Create game selection component -- [ ] Display games grouped by generation -- [ ] Show game artwork/logo for each option -- [ ] Add search/filter functionality for games -- [ ] Navigate to rule settings after selection -- [ ] Allow returning to change game selection +- [x] Create game selection component +- [x] Display games grouped by generation +- [x] Show game artwork/logo for each option +- [x] Add search/filter functionality for games +- [x] Navigate to rule settings after selection +- [x] Allow returning to change game selection -## UX Considerations -- Games should be visually recognizable -- Consider showing regional variants (e.g., Kanto games together) -- Mobile-friendly grid/list layout \ No newline at end of file +## Implementation Notes +- Game selection is step 1 of a 3-step new run wizard at `/runs/new` +- `GameCard` component: slug-based gradient backgrounds as fallback for missing box art +- `GameGrid` component: responsive grid with generation filter tabs +- `StepIndicator` component: clickable progress bar for navigating wizard steps +- `NewRun` page: wizard flow — select game → configure rules → name & create run +- Reuses existing `RulesConfiguration` component for step 2 +- `GameSelect` page at `/games` shows a browseable game grid with "Start New Run" CTA +- "New Run" link added to nav bar \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1d1473e..92e01e6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,14 +1,13 @@ import { Routes, Route } from 'react-router-dom' import { Layout } from './components' -import { Home, GameSelect, Dashboard, Encounters, Rules } from './pages' +import { Home, NewRun, Dashboard, Encounters } from './pages' function App() { return ( }> } /> - } /> - } /> + } /> } /> } /> diff --git a/frontend/src/components/GameCard.tsx b/frontend/src/components/GameCard.tsx new file mode 100644 index 0000000..01e59dc --- /dev/null +++ b/frontend/src/components/GameCard.tsx @@ -0,0 +1,79 @@ +import type { Game } from '../types' + +const GAME_GRADIENTS: Record = { + firered: 'from-red-500 to-orange-500', + leafgreen: 'from-green-500 to-emerald-500', + emerald: 'from-emerald-500 to-teal-500', + heartgold: 'from-amber-400 to-yellow-500', + soulsilver: 'from-gray-400 to-slate-500', +} + +const DEFAULT_GRADIENT = 'from-blue-500 to-indigo-500' + +interface GameCardProps { + game: Game + selected: boolean + onSelect: (game: Game) => void +} + +export function GameCard({ game, selected, onSelect }: GameCardProps) { + const gradient = GAME_GRADIENTS[game.slug] ?? DEFAULT_GRADIENT + + return ( + + ) +} diff --git a/frontend/src/components/GameGrid.tsx b/frontend/src/components/GameGrid.tsx new file mode 100644 index 0000000..dcfc29d --- /dev/null +++ b/frontend/src/components/GameGrid.tsx @@ -0,0 +1,94 @@ +import { useState, useMemo } from 'react' +import type { Game } from '../types' +import { GameCard } from './GameCard' + +const GENERATION_LABELS: Record = { + 1: 'Generation I', + 2: 'Generation II', + 3: 'Generation III', + 4: 'Generation IV', + 5: 'Generation V', + 6: 'Generation VI', + 7: 'Generation VII', + 8: 'Generation VIII', + 9: 'Generation IX', +} + +interface GameGridProps { + games: Game[] + selectedId: number | null + onSelect: (game: Game) => void +} + +export function GameGrid({ games, selectedId, onSelect }: GameGridProps) { + const [filter, setFilter] = useState(null) + + const generations = useMemo( + () => [...new Set(games.map((g) => g.generation))].sort(), + [games], + ) + + const filtered = filter + ? games.filter((g) => g.generation === filter) + : games + + const grouped = useMemo(() => { + const groups: Record = {} + for (const game of filtered) { + ;(groups[game.generation] ??= []).push(game) + } + return Object.entries(groups) + .map(([gen, games]) => ({ generation: Number(gen), games })) + .sort((a, b) => a.generation - b.generation) + }, [filtered]) + + return ( +
+
+ + {generations.map((gen) => ( + + ))} +
+ + {grouped.map(({ generation, games }) => ( +
+

+ {GENERATION_LABELS[generation] ?? `Generation ${generation}`} +

+
+ {games.map((game) => ( + + ))} +
+
+ ))} +
+ ) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index d06c24b..23673cc 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -13,16 +13,10 @@ export function Layout() {
- Games - - - Rules + New Run void +} + +export function StepIndicator({ currentStep, onStepClick }: StepIndicatorProps) { + return ( + + ) +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index bc8eb6a..ebc77dd 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,3 +1,6 @@ +export { GameCard } from './GameCard' +export { GameGrid } from './GameGrid' export { Layout } from './Layout' export { RuleToggle } from './RuleToggle' export { RulesConfiguration } from './RulesConfiguration' +export { StepIndicator } from './StepIndicator' diff --git a/frontend/src/pages/GameSelect.tsx b/frontend/src/pages/GameSelect.tsx deleted file mode 100644 index cb37dea..0000000 --- a/frontend/src/pages/GameSelect.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export function GameSelect() { - return ( -
-

Select a Game

-

- Game selection will be implemented here. -

-
- ) -} diff --git a/frontend/src/pages/NewRun.tsx b/frontend/src/pages/NewRun.tsx new file mode 100644 index 0000000..235a0bb --- /dev/null +++ b/frontend/src/pages/NewRun.tsx @@ -0,0 +1,188 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { GameGrid, RulesConfiguration, StepIndicator } from '../components' +import { useGames } from '../hooks/useGames' +import { useCreateRun } from '../hooks/useRuns' +import type { Game, NuzlockeRules } from '../types' +import { DEFAULT_RULES } from '../types' + +export function NewRun() { + const navigate = useNavigate() + const { data: games, isLoading, error } = useGames() + const createRun = useCreateRun() + + const [step, setStep] = useState(1) + const [selectedGame, setSelectedGame] = useState(null) + const [rules, setRules] = useState(DEFAULT_RULES) + const [runName, setRunName] = useState('') + + const handleGameSelect = (game: Game) => { + setSelectedGame(game) + if (!runName || runName === `${selectedGame?.name} Nuzlocke`) { + setRunName(`${game.name} Nuzlocke`) + } + } + + const handleCreate = () => { + if (!selectedGame) return + createRun.mutate( + { gameId: selectedGame.id, name: runName, rules }, + { onSuccess: () => navigate('/dashboard') }, + ) + } + + const enabledRuleCount = Object.values(rules).filter(Boolean).length + const totalRuleCount = Object.keys(rules).length + + return ( +
+

+ New Nuzlocke Run +

+

+ Set up your run in a few steps. +

+ + + + {step === 1 && ( +
+

+ Choose a Game +

+ + {isLoading && ( +
+
+
+ )} + + {error && ( +
+ Failed to load games. Please try again. +
+ )} + + {games && ( + + )} + +
+ +
+
+ )} + + {step === 2 && ( +
+ + +
+ + +
+
+ )} + + {step === 3 && ( +
+

+ Name Your Run +

+ +
+
+ + 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" + placeholder="My Nuzlocke Run" + /> +
+ +
+

+ Summary +

+
+
+
Game
+
+ {selectedGame?.name} +
+
+
+
Region
+
+ {selectedGame?.region} +
+
+
+
Rules
+
+ {enabledRuleCount} of {totalRuleCount} enabled +
+
+
+
+
+ + {createRun.error && ( +
+ Failed to create run. Please try again. +
+ )} + +
+ + +
+
+ )} +
+ ) +} diff --git a/frontend/src/pages/Rules.tsx b/frontend/src/pages/Rules.tsx deleted file mode 100644 index 3fd11d9..0000000 --- a/frontend/src/pages/Rules.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useState } from 'react' -import { RulesConfiguration } from '../components' -import type { NuzlockeRules } from '../types' -import { DEFAULT_RULES } from '../types' - -export function Rules() { - const [rules, setRules] = useState(DEFAULT_RULES) - const [saved, setSaved] = useState(false) - - const handleSave = () => { - // TODO: Persist to backend API - setSaved(true) - setTimeout(() => setSaved(false), 2000) - } - - return ( -
-
-

- Nuzlocke Rules -

-

- Configure the rules for your Nuzlocke run. Hover over the info icon - for explanations. -

-
- - - -
- - {saved && ( - - Rules saved! - - )} -
-
- ) -} diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index a236cd6..d70c66d 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -1,5 +1,4 @@ export { Home } from './Home' -export { GameSelect } from './GameSelect' +export { NewRun } from './NewRun' export { Dashboard } from './Dashboard' export { Encounters } from './Encounters' -export { Rules } from './Rules'