From ed7c6232c17b0ba76922b9d432e7798c0bc4678f Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Tue, 14 Apr 2026 15:22:56 +0200 Subject: [PATCH] refactor: streamline new-game player selection ux Consolidate new-game wizard steps into reusable components and remove legacy duplicate files. Replace inline player inputs with a chip-first flow and minimal name-entry modal, while improving compact-screen spacing and step-specific selection behavior. Made-with: Cursor --- README.md | 105 +++++++ src/components/NewGame.tsx | 77 ----- src/components/new-game/BreakOrderStep.tsx | 113 -------- src/components/new-game/BreakRuleStep.tsx | 55 ---- src/components/screens/NewGameScreen.tsx | 137 ++++++--- src/lib/features/README.md | 2 +- src/lib/features/new-game/NewGame.module.css | 104 +++++-- src/lib/features/new-game/README.md | 16 +- .../components/NameEntryModal.module.css | 66 +++++ .../new-game/components/NameEntryModal.tsx | 89 ++++++ .../new-game/components/PlayerNameStep.tsx | 231 +++++++++++++++ .../new-game/components/WizardNav.tsx | 45 +++ .../new-game/components/WizardStepForm.tsx | 36 +++ src/lib/features/new-game/index.ts | 4 +- .../new-game/steps/BreakOrderStep.tsx | 58 ++-- .../features/new-game/steps/BreakRuleStep.tsx | 35 +-- .../features/new-game/steps/GameTypeStep.tsx | 54 +--- .../features/new-game/steps/Player1Step.tsx | 265 ------------------ .../features/new-game/steps/Player2Step.tsx | 155 ---------- .../features/new-game/steps/Player3Step.tsx | 161 ----------- .../features/new-game/steps/RaceToStep.tsx | 35 +-- 21 files changed, 825 insertions(+), 1018 deletions(-) delete mode 100644 src/components/NewGame.tsx delete mode 100644 src/components/new-game/BreakOrderStep.tsx delete mode 100644 src/components/new-game/BreakRuleStep.tsx create mode 100644 src/lib/features/new-game/components/NameEntryModal.module.css create mode 100644 src/lib/features/new-game/components/NameEntryModal.tsx create mode 100644 src/lib/features/new-game/components/PlayerNameStep.tsx create mode 100644 src/lib/features/new-game/components/WizardNav.tsx create mode 100644 src/lib/features/new-game/components/WizardStepForm.tsx delete mode 100644 src/lib/features/new-game/steps/Player1Step.tsx delete mode 100644 src/lib/features/new-game/steps/Player2Step.tsx delete mode 100644 src/lib/features/new-game/steps/Player3Step.tsx diff --git a/README.md b/README.md index fb751ea..08db00c 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,111 @@ npm run test:record npm run test:e2e ``` +### Seed test matches via browser console + +Copy/paste this snippet in your browser devtools console while the app is open. It inserts 10 sample matches (mix of 2/3 players, 8/9/10-ball, completed + active) into IndexedDB and reloads the page. + +```js +await (async () => { + const DB_NAME = 'BSCScoreDB'; + const DB_VERSION = 1; + const GAMES_STORE = 'games'; + + const openDb = () => + new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + const now = Date.now(); + const minute = 60 * 1000; + + const seedGames = [ + { players: ['Alex', 'Ben'], gameType: '8-Ball', raceTo: 5, scores: [5, 2], status: 'completed', minutesAgo: 190 }, + { players: ['Chris', 'Dana', 'Eli'], gameType: '8-Ball', raceTo: 6, scores: [4, 6, 2], status: 'completed', minutesAgo: 175 }, + { players: ['Faye', 'Gus'], gameType: '9-Ball', raceTo: 7, scores: [7, 5], status: 'completed', minutesAgo: 160 }, + { players: ['Hugo', 'Iris', 'Jade'], gameType: '9-Ball', raceTo: 4, scores: [2, 3, 1], status: 'active', minutesAgo: 145 }, + { players: ['Kai', 'Lena'], gameType: '10-Ball', raceTo: 8, scores: [8, 6], status: 'completed', minutesAgo: 130 }, + { players: ['Mona', 'Nico', 'Omar'], gameType: '10-Ball', raceTo: 5, scores: [5, 1, 4], status: 'completed', minutesAgo: 115 }, + { players: ['Pia', 'Quin'], gameType: '8-Ball', raceTo: 4, scores: [1, 1], status: 'active', minutesAgo: 100 }, + { players: ['Rex', 'Sara', 'Timo'], gameType: '9-Ball', raceTo: 3, scores: [3, 2, 0], status: 'completed', minutesAgo: 85 }, + { players: ['Uma', 'Vik'], gameType: '10-Ball', raceTo: 6, scores: [2, 4], status: 'active', minutesAgo: 70 }, + { players: ['Will', 'Xena', 'Yara'], gameType: '8-Ball', raceTo: 7, scores: [6, 4, 7], status: 'completed', minutesAgo: 55 }, + ]; + + const gameRecords = seedGames.map((entry, index) => { + const createdAtMs = now - entry.minutesAgo * minute; + const updatedAtMs = createdAtMs + 15 * 1000; + const id = now + index + 1; + const isThreePlayer = entry.players.length === 3; + const winnerIndex = entry.scores.indexOf(Math.max(...entry.scores)); + + const game = { + id, + gameType: entry.gameType, + raceTo: entry.raceTo, + status: entry.status, + createdAt: new Date(createdAtMs).toISOString(), + updatedAt: new Date(updatedAtMs).toISOString(), + log: [ + { + type: 'score_change', + timestamp: new Date(createdAtMs + 5 * 1000).toISOString(), + description: 'Seed data: score progression', + }, + ...(entry.status === 'completed' + ? [ + { + type: 'game_complete', + timestamp: new Date(updatedAtMs).toISOString(), + description: `Winner: ${entry.players[winnerIndex]}`, + }, + ] + : []), + ], + undoStack: [], + player1: entry.players[0], + player2: entry.players[1], + ...(isThreePlayer ? { player3: entry.players[2] } : {}), + score1: entry.scores[0], + score2: entry.scores[1], + ...(isThreePlayer ? { score3: entry.scores[2] } : {}), + breakRule: 'winnerbreak', + breakOrder: isThreePlayer ? [1, 2, 3] : [1, 2], + currentBreakerIdx: index % entry.players.length, + }; + + return { + id, + game, + lastModified: updatedAtMs, + syncStatus: 'local', + version: 1, + createdAt: createdAtMs, + }; + }); + + const db = await openDb(); + + await new Promise((resolve, reject) => { + const tx = db.transaction([GAMES_STORE], 'readwrite'); + const store = tx.objectStore(GAMES_STORE); + + for (const record of gameRecords) { + store.put(record); + } + + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error); + }); + + console.log(`Inserted ${gameRecords.length} test matches into IndexedDB.`); + window.location.reload(); +})(); +``` + ### Features - **Record interactions**: Use Playwright codegen to capture clicks, form fills, and navigation diff --git a/src/components/NewGame.tsx b/src/components/NewGame.tsx deleted file mode 100644 index 3c43bf0..0000000 --- a/src/components/NewGame.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { h } from 'preact'; -import { useState, useEffect, useRef } from 'preact/hooks'; -import styles from './NewGame.module.css'; -import modalStyles from './PlayerSelectModal.module.css'; -import { PlayerSelectModal } from './new-game/PlayerSelectModal'; -import { - UI_CONSTANTS, - WIZARD_STEPS, - GAME_TYPE_OPTIONS, - RACE_TO_QUICK_PICKS, - RACE_TO_DEFAULT, - RACE_TO_INFINITY, - ERROR_MESSAGES, - ARIA_LABELS, - FORM_CONFIG, - ERROR_STYLES -} from '../utils/constants'; -import { Player1Step } from './new-game/Player1Step'; -import { Player2Step } from './new-game/Player2Step'; -import { Player3Step } from './new-game/Player3Step'; -import { GameTypeStep } from './new-game/GameTypeStep'; -import { RaceToStep } from './new-game/RaceToStep'; -import { BreakRuleStep } from './new-game/BreakRuleStep'; -import { BreakOrderStep } from './new-game/BreakOrderStep'; -import type { BreakRule } from '@lib/domain/types'; - -// PlayerSelectModal moved to ./new-game/PlayerSelectModal - -interface PlayerStepProps { - playerNameHistory: string[]; - onNext: (name: string) => void; - onCancel: () => void; - initialValue?: string; -} - -// Player1Step moved to ./new-game/Player1Step - -// Player2Step moved to ./new-game/Player2Step - -// Player3Step moved to ./new-game/Player3Step - -interface GameTypeStepProps { - onNext: (type: string) => void; - onCancel: () => void; - initialValue?: string; -} - - -interface RaceToStepProps { - onNext: (raceTo: string | number) => void; - onCancel: () => void; - initialValue?: string | number; - gameType?: string; -} - -// GameTypeStep and RaceToStep moved to ./new-game - -interface BreakRuleStepProps { - onNext: (rule: BreakRule) => void; - onCancel: () => void; - initialValue?: BreakRule | 'winnerbreak'; -} - -// BreakRuleStep moved to ./new-game/BreakRuleStep - -interface BreakOrderStepProps { - players: string[]; - rule: BreakRule; - onNext: (first: number, second?: number) => void; - onCancel: () => void; - initialFirst?: number; - initialSecond?: number; -} - -// BreakOrderStep moved to ./new-game/BreakOrderStep - -export { Player1Step, Player2Step, Player3Step, GameTypeStep, BreakRuleStep, BreakOrderStep, RaceToStep }; \ No newline at end of file diff --git a/src/components/new-game/BreakOrderStep.tsx b/src/components/new-game/BreakOrderStep.tsx deleted file mode 100644 index 6e00c0b..0000000 --- a/src/components/new-game/BreakOrderStep.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { h } from 'preact'; -import { useEffect, useState } from 'preact/hooks'; -import styles from '../NewGame.module.css'; -import type { BreakRule } from '@lib/domain/types'; - -interface BreakOrderStepProps { - players: string[]; - rule: BreakRule; - onNext: (first: number, second?: number) => void; - onCancel: () => void; - initialFirst?: number; - initialSecond?: number; -} - -export const BreakOrderStep = ({ players, rule, onNext, onCancel, initialFirst = 1, initialSecond }: BreakOrderStepProps) => { - const playerCount = players.filter(Boolean).length; - const [first, setFirst] = useState(initialFirst); - const [second, setSecond] = useState(initialSecond); - - useEffect(() => { - if (!initialSecond && rule === 'wechselbreak' && playerCount === 3) { - setSecond(2); - } - }, [initialSecond, rule, playerCount]); - - const handleFirst = (idx: number) => { - setFirst(idx); - if (rule === 'winnerbreak' || (rule === 'wechselbreak' && playerCount === 2)) { - onNext(idx); - } - }; - - const handleSecond = (idx: number) => { - setSecond(idx); - onNext(first, idx); - }; - - return ( -
-
Wer hat den ersten Anstoss?
-
- - - - - - - -
-
Wer hat den ersten Anstoss?
-
- {players.filter(Boolean).map((name, idx) => ( - - ))} -
- {rule === 'wechselbreak' && playerCount === 3 && ( - <> -
Wer hat den zweiten Anstoss?
-
- {players.filter(Boolean).map((name, idx) => ( - - ))} -
- - )} -
- - -
-
- ); -}; - - diff --git a/src/components/new-game/BreakRuleStep.tsx b/src/components/new-game/BreakRuleStep.tsx deleted file mode 100644 index 1f0656d..0000000 --- a/src/components/new-game/BreakRuleStep.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { h } from 'preact'; -import { useState } from 'preact/hooks'; -import styles from '../NewGame.module.css'; -import type { BreakRule } from '@lib/domain/types'; - -interface BreakRuleStepProps { - onNext: (rule: BreakRule) => void; - onCancel: () => void; - initialValue?: BreakRule; -} - -export const BreakRuleStep = ({ onNext, onCancel, initialValue = 'winnerbreak' }: BreakRuleStepProps) => { - const [rule, setRule] = useState(initialValue ?? 'winnerbreak'); - - return ( -
-
Break-Regel wählen
-
- - - - - - - -
-
- {[ - { key: 'winnerbreak', label: 'Winnerbreak' }, - { key: 'wechselbreak', label: 'Wechselbreak' }, - ].map(opt => ( - - ))} -
-
- - -
-
- ); -}; - - diff --git a/src/components/screens/NewGameScreen.tsx b/src/components/screens/NewGameScreen.tsx index 5962425..44e21e9 100644 --- a/src/components/screens/NewGameScreen.tsx +++ b/src/components/screens/NewGameScreen.tsx @@ -1,16 +1,17 @@ import { h } from 'preact'; +import type { ComponentProps } from 'preact'; +import { useEffect, useMemo, useState } from 'preact/hooks'; import { Screen } from '@lib/ui/Layout'; import { - Player1Step, - Player2Step, - Player3Step, + PlayerNameStep, GameTypeStep, BreakRuleStep, BreakOrderStep, RaceToStep, } from '@lib/features/new-game'; import type { NewGameStep, NewGameData, GameType } from '@lib/domain/types'; -import { GAME_TYPES, RACE_TO_DEFAULT } from '@lib/domain/constants'; +import { ERROR_MESSAGES, GAME_TYPES, RACE_TO_DEFAULT } from '@lib/domain/constants'; +import newGameStyles from '@lib/features/new-game/NewGame.module.css'; interface NewGameScreenProps { step: NewGameStep; @@ -23,6 +24,10 @@ interface NewGameScreenProps { onShowValidation: (message: string) => void; } +type PlayerStepKey = 'player1' | 'player2' | 'player3'; +type PlayerStepConfig = Omit, 'playerNameHistory'>; +type PlayerStepConfigMap = Record; + export default function NewGameScreen({ step, data, @@ -33,19 +38,29 @@ export default function NewGameScreen({ onCancel, onShowValidation, }: NewGameScreenProps) { - const handlePlayer1Next = (name: string) => { - onDataChange({ player1: name }); - onStepChange('player2'); - }; + const [temporaryPlayerNames, setTemporaryPlayerNames] = useState([]); - const handlePlayer2Next = (name: string) => { - onDataChange({ player2: name }); - onStepChange('player3'); - }; + useEffect(() => { + // Reset wizard-local names when a fresh wizard starts at player 1. + if ( + step === 'player1' && + !data.player1 && + !data.player2 && + !data.player3 + ) { + setTemporaryPlayerNames([]); + } + }, [step, data.player1, data.player2, data.player3]); - const handlePlayer3Next = (name: string) => { - onDataChange({ player3: name }); - onStepChange('gameType'); + const playerNameOptions = useMemo( + () => Array.from(new Set([...temporaryPlayerNames, ...playerHistory])), + [temporaryPlayerNames, playerHistory], + ); + + const addTemporaryPlayerName = (name: string) => { + setTemporaryPlayerNames((prev) => + prev.includes(name) ? prev : [name, ...prev], + ); }; const handleGameTypeNext = (type: string) => { @@ -79,7 +94,6 @@ export default function NewGameScreen({ const handleRaceToNext = (raceTo: string | number) => { // Convert to string, handling Infinity case explicitly const raceToStr = raceTo === Infinity ? 'Infinity' : String(raceTo); - const finalData = { ...data, raceTo: raceToStr }; // After race to, go to break rule selection onDataChange({ raceTo: raceToStr }); onStepChange('breakRule'); @@ -110,32 +124,75 @@ export default function NewGameScreen({ } }; + const playerStepConfig: PlayerStepConfigMap = { + player1: { + stepNumber: 1, + title: 'Name Spieler 1', + label: 'Spieler 1', + placeholder: 'Name Spieler 1', + inputId: 'player1-input', + playerInputClassName: newGameStyles['player1-input'], + initialValue: data.player1, + onNext: (name: string) => { + onDataChange({ player1: name }); + onStepChange('player2'); + }, + onCancel, + required: true, + requiredErrorMessage: ERROR_MESSAGES.PLAYER1_REQUIRED, + enterKeyHint: 'next', + showMorePlayersModal: true, + }, + player2: { + stepNumber: 2, + title: 'Name Spieler 2', + label: 'Spieler 2', + placeholder: 'Name Spieler 2', + inputId: 'player2-input', + playerInputClassName: newGameStyles['player2-input'], + initialValue: data.player2, + onNext: (name: string) => { + onDataChange({ player2: name }); + onStepChange('player3'); + }, + onCancel: handleStepBack, + required: true, + requiredErrorMessage: ERROR_MESSAGES.PLAYER2_REQUIRED, + enterKeyHint: 'next', + showMorePlayersModal: true, + prefillNameEntry: false, + onCreateTemporaryName: addTemporaryPlayerName, + }, + player3: { + stepNumber: 3, + title: 'Name Spieler 3 (optional)', + label: 'Spieler 3 (optional)', + placeholder: 'Name Spieler 3 (optional)', + inputId: 'player3-input', + playerInputClassName: newGameStyles['player3-input'], + initialValue: data.player3, + onNext: (name: string) => { + onDataChange({ player3: name }); + onStepChange('gameType'); + }, + onCancel: handleStepBack, + required: false, + enterKeyHint: 'done', + showSkipButton: true, + skipLabel: 'Überspringen', + showMorePlayersModal: true, + prefillNameEntry: false, + onCreateTemporaryName: addTemporaryPlayerName, + }, + }; + return ( - {step === 'player1' && ( - - )} - - {step === 'player2' && ( - - )} - - {step === 'player3' && ( - )} diff --git a/src/lib/features/README.md b/src/lib/features/README.md index f8f346c..0039e1a 100644 --- a/src/lib/features/README.md +++ b/src/lib/features/README.md @@ -11,7 +11,7 @@ Feature directories compose domain, data, state, and UI primitives into end-user - `game-lifecycle` - `GameCompletionModal` summarising winners + rematch CTA. - `new-game` - - Wizard step components (`Player1Step`, `BreakOrderStep`, etc.) and modal pickers. + - Wizard step components (`PlayerNameStep`, `BreakOrderStep`, etc.) and modal pickers. ## Usage Example diff --git a/src/lib/features/new-game/NewGame.module.css b/src/lib/features/new-game/NewGame.module.css index 58f82fe..3aa878e 100644 --- a/src/lib/features/new-game/NewGame.module.css +++ b/src/lib/features/new-game/NewGame.module.css @@ -140,7 +140,7 @@ .form-header { flex-shrink: 0; - padding: clamp(0.5rem, 2vh, 2rem) var(--space-lg) clamp(0.25rem, 1vh, 1rem) var(--space-lg); + padding: clamp(0.4rem, 1.5vh, 1.25rem) var(--space-lg) clamp(0.2rem, 0.8vh, 0.75rem) var(--space-lg); } .form-content { @@ -148,7 +148,7 @@ overflow-y: auto; overscroll-behavior: contain; padding: 0 var(--space-lg); - padding-bottom: var(--space-md); + padding-bottom: var(--space-sm); min-height: 0; display: flex; flex-direction: column; @@ -160,7 +160,7 @@ bottom: 0; z-index: 2; background: var(--color-surface); - padding: var(--space-md) var(--space-lg) calc(var(--space-lg) + var(--keyboard-offset)) var(--space-lg); + padding: var(--space-sm) var(--space-lg) calc(var(--space-sm) + var(--keyboard-offset)) var(--space-lg); border-top: 1px solid var(--color-border); } .progress-indicator { @@ -195,14 +195,14 @@ .quick-pick-btn { min-width: 60px; - min-height: 36px; + min-height: 32px; font-size: clamp(0.75rem, 2vw, 1rem); border-radius: var(--radius-md); background: var(--color-secondary); color: var(--color-text); border: 1px solid var(--color-border); cursor: pointer; - padding: 0.4rem 0.8rem; + padding: 0.3rem 0.7rem; transition: all var(--transition-base); font-weight: 500; flex-shrink: 0; @@ -217,14 +217,19 @@ display: flex; justify-content: space-between; align-items: center; - margin-top: var(--space-lg); + margin-top: var(--space-xs); width: 100%; - gap: var(--space-lg); + gap: var(--space-sm); +} +.arrow-nav-actions { + display: flex; + align-items: center; + gap: var(--space-md); } .arrow-btn { - font-size: 48px; - width: 80px; - height: 80px; + font-size: 28px; + width: 44px; + height: 44px; border-radius: 50%; background: var(--color-secondary); color: var(--color-text); @@ -237,6 +242,11 @@ transition: all var(--transition-base); font-weight: bold; } +.arrow-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} .arrow-btn:hover, .arrow-btn:focus { background: var(--color-secondary-hover); border-color: var(--color-primary); @@ -272,6 +282,44 @@ background: var(--color-secondary); outline: none; } +.name-input-wrapper { + position: relative; + width: 100%; +} +.character-counter { + font-size: 0.875rem; + margin-top: 4px; + text-align: right; +} +.quick-picks-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 4px; +} +.quick-pick-action-btn { + background: var(--color-primary); + border-color: var(--color-primary); + color: #fff; +} +.quick-pick-action-btn:hover, +.quick-pick-action-btn:focus { + background: var(--color-primary-hover); + border-color: var(--color-primary-hover); +} +.quick-pick-actions-separator { + width: 10px; + height: 1px; + margin: 0 4px; +} +.choice-heading { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; +} +.choice-heading:first-child { + margin-top: 0; +} .game-type-selection { display: grid; grid-template-columns: 1fr 1fr; @@ -397,9 +445,9 @@ font-size: var(--font-size-xxxl); } .arrow-btn { - width: 100px; - height: 100px; - font-size: 56px; + width: 56px; + height: 56px; + font-size: 34px; } .game-type-btn, .race-to-btn { @@ -419,7 +467,7 @@ } .new-game-form { margin: var(--space-lg) auto 0 auto; - padding: var(--space-lg); + padding: var(--space-sm); } .game-type-selection { grid-template-columns: 1fr; @@ -431,8 +479,28 @@ gap: var(--space-md); } .arrow-btn { - width: 70px; - height: 70px; - font-size: 40px; + width: 40px; + height: 40px; + font-size: 24px; } -} \ No newline at end of file +} + +@media (max-height: 560px) { + .screen-title { + margin-bottom: 0.35rem; + } + .progress-indicator { + margin-bottom: 0.35rem; + } + .player-input { + padding: var(--space-sm); + } + .form-header { + padding-top: 0.4rem; + padding-bottom: 0.3rem; + } + .form-footer { + padding-top: 0.3rem; + padding-bottom: calc(0.35rem + var(--keyboard-offset)); + } +} \ No newline at end of file diff --git a/src/lib/features/new-game/README.md b/src/lib/features/new-game/README.md index efb9c80..1de72ee 100644 --- a/src/lib/features/new-game/README.md +++ b/src/lib/features/new-game/README.md @@ -4,7 +4,7 @@ Composable building blocks for the multi-step "start a new game" workflow. ## Exports -- `Player1Step`, `Player2Step`, `Player3Step` – Player name capture with history + quick picks. +- `PlayerNameStep` – Configurable player name capture step with history + quick picks. - `GameTypeStep` – Game type selector. - `RaceToStep` – Numeric race-to chooser with infinity support. - `BreakRuleStep`, `BreakOrderStep` – Break configuration helpers. @@ -21,7 +21,7 @@ All exports are surfaced via `@lib/features/new-game`. ## Integrating the Wizard ```tsx -import { Player1Step, Player2Step } from '@lib/features/new-game'; +import { PlayerNameStep } from '@lib/features/new-game'; import { useNewGameWizard } from '@lib/state'; const wizard = useNewGameWizard(); @@ -29,13 +29,23 @@ const wizard = useNewGameWizard(); return ( <> {wizard.newGameStep === 'player1' && ( - { wizard.updateGameData({ player1: name }); wizard.nextStep('player2'); }} onCancel={wizard.resetWizard} + required + requiredErrorMessage="Bitte Namen für Spieler 1 eingeben" + enterKeyHint="next" + showMorePlayersModal /> )} {/* render subsequent steps analogously */} diff --git a/src/lib/features/new-game/components/NameEntryModal.module.css b/src/lib/features/new-game/components/NameEntryModal.module.css new file mode 100644 index 0000000..172f7b0 --- /dev/null +++ b/src/lib/features/new-game/components/NameEntryModal.module.css @@ -0,0 +1,66 @@ +.modalOverlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 16px; +} + +.modalContent { + width: min(420px, 100%); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.modalTitle { + margin: 0; + font-size: 1.1rem; + color: var(--color-text); +} + +.modalInput { + width: 100%; +} + +.modalActions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.actionButton { + border: 1px solid var(--color-border); + background: var(--color-secondary); + color: var(--color-text); + border-radius: var(--radius-md); + padding: 10px 14px; + cursor: pointer; + min-height: var(--touch-target-min); +} + +.actionButton:hover, +.actionButton:focus { + border-color: var(--color-primary); + outline: none; +} + +.confirmButton { + background: var(--color-primary); + border-color: var(--color-primary); + color: #fff; +} + +.errorText { + color: var(--color-danger); + font-size: 0.9rem; + margin: 0; +} diff --git a/src/lib/features/new-game/components/NameEntryModal.tsx b/src/lib/features/new-game/components/NameEntryModal.tsx new file mode 100644 index 0000000..8c80f9f --- /dev/null +++ b/src/lib/features/new-game/components/NameEntryModal.tsx @@ -0,0 +1,89 @@ +import { h } from 'preact'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import styles from './NameEntryModal.module.css'; +import newGameStyles from '../NewGame.module.css'; + +interface NameEntryModalProps { + open: boolean; + title: string; + placeholder: string; + inputId: string; + initialValue?: string; + enterKeyHint?: 'next' | 'done'; + onClose: () => void; + onConfirm: (name: string) => string | null; +} + +export function NameEntryModal({ + open, + title, + placeholder, + inputId, + initialValue = '', + enterKeyHint = 'done', + onClose, + onConfirm, +}: NameEntryModalProps) { + const [name, setName] = useState(initialValue); + const [error, setError] = useState(null); + const inputRef = useRef(null); + + useEffect(() => { + if (!open) return; + setName(initialValue); + setError(null); + window.setTimeout(() => inputRef.current?.focus(), 0); + }, [open, initialValue]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()}> +

+ {title} +

+ + { + setName((e.target as HTMLInputElement).value); + if (error) setError(null); + }} + onKeyDown={(e) => { + if (e.key !== 'Enter') return; + e.preventDefault(); + const validationError = onConfirm(name); + if (validationError) setError(validationError); + }} + /> + + {error &&

{error}

} + +
+ + +
+
+
+ ); +} diff --git a/src/lib/features/new-game/components/PlayerNameStep.tsx b/src/lib/features/new-game/components/PlayerNameStep.tsx new file mode 100644 index 0000000..8ca4fc5 --- /dev/null +++ b/src/lib/features/new-game/components/PlayerNameStep.tsx @@ -0,0 +1,231 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import styles from '../NewGame.module.css'; +import { + ARIA_LABELS, + ERROR_STYLES, + FORM_CONFIG, + UI_CONSTANTS, +} from '@lib/domain/constants'; +import { PlayerSelectModal } from '../steps/PlayerSelectModal'; +import { NameEntryModal } from './NameEntryModal'; +import { WizardNav } from './WizardNav'; +import { WizardStepForm } from './WizardStepForm'; + +interface PlayerNameStepProps { + stepNumber: number; + title: string; + label: string; + placeholder: string; + inputId: string; + playerInputClassName: string; + playerNameHistory: string[]; + onNext: (name: string) => void; + onCancel: () => void; + initialValue?: string; + required?: boolean; + requiredErrorMessage?: string; + enterKeyHint: 'next' | 'done'; + showSkipButton?: boolean; + skipLabel?: string; + showMorePlayersModal?: boolean; + prefillNameEntry?: boolean; + onCreateTemporaryName?: (name: string) => void; +} + +export function PlayerNameStep({ + stepNumber, + title, + label, + placeholder, + inputId, + playerInputClassName, + playerNameHistory, + onNext, + onCancel, + initialValue = '', + required = true, + requiredErrorMessage, + enterKeyHint, + showSkipButton = false, + skipLabel = 'Überspringen', + showMorePlayersModal = false, + prefillNameEntry = true, + onCreateTemporaryName, +}: PlayerNameStepProps) { + const [value, setValue] = useState(initialValue); + const [error, setError] = useState(null); + const [isPlayerListOpen, setIsPlayerListOpen] = useState(false); + const [isNameEntryOpen, setIsNameEntryOpen] = useState(false); + + const validate = (name: string): string | null => { + const trimmedName = name.trim(); + + if (required && !trimmedName) { + return requiredErrorMessage ?? 'Bitte Namen eingeben'; + } + + if (trimmedName.length > FORM_CONFIG.MAX_PLAYER_NAME_LENGTH) { + return `Spielername darf maximal ${FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen lang sein`; + } + + return null; + }; + + const handleSubmit = (e: Event) => { + e.preventDefault(); + const trimmedValue = value.trim(); + const validationError = validate(trimmedValue); + + if (validationError) { + setError(validationError); + return; + } + + setError(null); + onNext(trimmedValue); + }; + + const handleQuickPick = (name: string) => { + setValue(name); + setError(null); + onNext(name); + }; + + const handleSkip = () => { + setValue(''); + setError(null); + onNext(''); + }; + + const handleManualNameSubmit = (name: string): string | null => { + const trimmedName = name.trim(); + if (!trimmedName) { + return 'Bitte Namen eingeben'; + } + + const manualNameError = validate(trimmedName); + if (manualNameError) { + return manualNameError; + } + + setValue(trimmedName); + setError(null); + setIsNameEntryOpen(false); + onCreateTemporaryName?.(trimmedName); + onNext(trimmedName); + return null; + }; + + return ( + + {skipLabel} + + ) : undefined + } + /> + } + > +
+
+ {showMorePlayersModal && playerNameHistory.length > 0 && ( + + )} + + + + {playerNameHistory.length > 0 && ( +
+
+ + {error && ( +
+ ⚠️ + {error} +
+ )} + + {isPlayerListOpen && ( + { + setIsPlayerListOpen(false); + handleQuickPick(name); + }} + onClose={() => setIsPlayerListOpen(false)} + /> + )} + + setIsNameEntryOpen(false)} + onConfirm={handleManualNameSubmit} + /> + + {value.length > FORM_CONFIG.CHARACTER_COUNT_WARNING_THRESHOLD && ( +
FORM_CONFIG.MAX_PLAYER_NAME_LENGTH ? '#f44336' : '#ff9800' }} + > + {value.length}/{FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen +
+ )} +
+ ); +} diff --git a/src/lib/features/new-game/components/WizardNav.tsx b/src/lib/features/new-game/components/WizardNav.tsx new file mode 100644 index 0000000..c6e50b8 --- /dev/null +++ b/src/lib/features/new-game/components/WizardNav.tsx @@ -0,0 +1,45 @@ +import { h } from 'preact'; +import type { ComponentChildren } from 'preact'; +import styles from '../NewGame.module.css'; +import { ARIA_LABELS } from '@lib/domain/constants'; + +interface WizardNavProps { + onBack: () => void; + nextDisabled?: boolean; + nextType?: 'submit' | 'button'; + nextButtonAriaLabel?: string; + middleContent?: ComponentChildren; +} + +export function WizardNav({ + onBack, + nextDisabled = false, + nextType = 'submit', + nextButtonAriaLabel = ARIA_LABELS.NEXT, + middleContent, +}: WizardNavProps) { + return ( +
+ + +
+ {middleContent} + +
+
+ ); +} diff --git a/src/lib/features/new-game/components/WizardStepForm.tsx b/src/lib/features/new-game/components/WizardStepForm.tsx new file mode 100644 index 0000000..963c081 --- /dev/null +++ b/src/lib/features/new-game/components/WizardStepForm.tsx @@ -0,0 +1,36 @@ +import { h } from 'preact'; +import type { ComponentChildren } from 'preact'; +import { ProgressIndicator } from '../ProgressIndicator'; +import styles from '../NewGame.module.css'; +import { UI_CONSTANTS } from '@lib/domain/constants'; + +interface WizardStepFormProps { + ariaLabel: string; + title: string; + currentStep: number; + onSubmit?: (e: Event) => void; + children: ComponentChildren; + footer: ComponentChildren; +} + +export function WizardStepForm({ + ariaLabel, + title, + currentStep, + onSubmit, + children, + footer, +}: WizardStepFormProps) { + return ( +
+
+
{title}
+ +
+ +
{children}
+ +
{footer}
+
+ ); +} diff --git a/src/lib/features/new-game/index.ts b/src/lib/features/new-game/index.ts index dc84ee7..785e57d 100644 --- a/src/lib/features/new-game/index.ts +++ b/src/lib/features/new-game/index.ts @@ -1,7 +1,5 @@ export { PlayerSelectModal } from './steps/PlayerSelectModal'; -export { Player1Step } from './steps/Player1Step'; -export { Player2Step } from './steps/Player2Step'; -export { Player3Step } from './steps/Player3Step'; +export { PlayerNameStep } from './components/PlayerNameStep'; export { GameTypeStep } from './steps/GameTypeStep'; export { RaceToStep } from './steps/RaceToStep'; export { BreakRuleStep } from './steps/BreakRuleStep'; diff --git a/src/lib/features/new-game/steps/BreakOrderStep.tsx b/src/lib/features/new-game/steps/BreakOrderStep.tsx index c192c74..7708124 100644 --- a/src/lib/features/new-game/steps/BreakOrderStep.tsx +++ b/src/lib/features/new-game/steps/BreakOrderStep.tsx @@ -1,8 +1,9 @@ import { h } from 'preact'; import { useEffect, useState } from 'preact/hooks'; import styles from '../NewGame.module.css'; -import { ProgressIndicator } from '../ProgressIndicator'; import type { BreakRule } from '@lib/domain/types'; +import { WizardNav } from '../components/WizardNav'; +import { WizardStepForm } from '../components/WizardStepForm'; interface BreakOrderStepProps { players: string[]; @@ -48,15 +49,22 @@ export const BreakOrderStep = ({ players, rule, onNext, onCancel, initialFirst = }; return ( -
-
-
Wer hat den ersten Anstoss?
- -
- -
-
Wer hat den ersten Anstoss?
-
+ 0 && (second ?? 0) > 0) : !(first > 0) + } + /> + } + > +
Wer hat den ersten Anstoss?
+
{players.filter(Boolean).map((name, idx) => ( @@ -73,8 +81,8 @@ export const BreakOrderStep = ({ players, rule, onNext, onCancel, initialFirst = {rule === 'wechselbreak' && playerCount === 3 && ( <> -
Wer hat den zweiten Anstoss?
-
+
Wer hat den zweiten Anstoss?
+
{players.filter(Boolean).map((name, idx) => ( @@ -90,27 +98,7 @@ export const BreakOrderStep = ({ players, rule, onNext, onCancel, initialFirst =
)} -
- -
-
- - -
-
- + ); }; diff --git a/src/lib/features/new-game/steps/BreakRuleStep.tsx b/src/lib/features/new-game/steps/BreakRuleStep.tsx index e6d81f4..c87c97a 100644 --- a/src/lib/features/new-game/steps/BreakRuleStep.tsx +++ b/src/lib/features/new-game/steps/BreakRuleStep.tsx @@ -1,8 +1,9 @@ import { h } from 'preact'; import { useState } from 'preact/hooks'; import styles from '../NewGame.module.css'; -import { ProgressIndicator } from '../ProgressIndicator'; import type { BreakRule } from '@lib/domain/types'; +import { WizardNav } from '../components/WizardNav'; +import { WizardStepForm } from '../components/WizardStepForm'; interface BreakRuleStepProps { onNext: (rule: BreakRule) => void; @@ -23,14 +24,14 @@ export const BreakRuleStep = ({ onNext, onCancel, initialValue = 'winnerbreak' } }; return ( -
-
-
Break-Regel wählen
- -
- -
-
+ } + > +
{[ { key: 'winnerbreak', label: 'Winnerbreak' }, { key: 'wechselbreak', label: 'Wechselbreak' }, @@ -41,25 +42,13 @@ export const BreakRuleStep = ({ onNext, onCancel, initialValue = 'winnerbreak' } className={`${styles['quick-pick-btn']} ${rule === (opt.key as BreakRule) ? styles['selected'] : ''}`} onClick={() => handleSelect(opt.key as BreakRule)} aria-label={`Break-Regel wählen: ${opt.label}`} - style={{ minWidth: 160, minHeight: 64, fontSize: '1.2rem', padding: '16px 32px' }} + style={{ minWidth: 160, minHeight: 64 }} > {opt.label} ))}
-
- -
-
- - -
-
- + ); }; diff --git a/src/lib/features/new-game/steps/GameTypeStep.tsx b/src/lib/features/new-game/steps/GameTypeStep.tsx index 2a1f31a..a3497f2 100644 --- a/src/lib/features/new-game/steps/GameTypeStep.tsx +++ b/src/lib/features/new-game/steps/GameTypeStep.tsx @@ -1,8 +1,9 @@ import { h } from 'preact'; import { useState } from 'preact/hooks'; import styles from '../NewGame.module.css'; -import { ProgressIndicator } from '../ProgressIndicator'; import { GAME_TYPES } from '@lib/domain/constants'; +import { WizardNav } from '../components/WizardNav'; +import { WizardStepForm } from '../components/WizardStepForm'; interface GameTypeStepProps { onNext: (type: string) => void; @@ -25,13 +26,13 @@ export const GameTypeStep = ({ onNext, onCancel, initialValue = '' }: GameTypeSt }; return ( -
-
-
Spielart auswählen
- -
- -
+ } + >
{GAME_TYPES.map(({ value, label }) => (
-
- -
-
- - -
-
-
+ ); }; diff --git a/src/lib/features/new-game/steps/Player1Step.tsx b/src/lib/features/new-game/steps/Player1Step.tsx deleted file mode 100644 index 9f8444a..0000000 --- a/src/lib/features/new-game/steps/Player1Step.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { h } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import styles from '../NewGame.module.css'; -import { - UI_CONSTANTS, - ERROR_MESSAGES, - ARIA_LABELS, - FORM_CONFIG, - ERROR_STYLES, -} from '@lib/domain/constants'; -import { PlayerSelectModal } from './PlayerSelectModal'; -import { ProgressIndicator } from '../ProgressIndicator'; - -interface PlayerStepProps { - playerNameHistory: string[]; - onNext: (name: string) => void; - onCancel: () => void; - initialValue?: string; -} - -export const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }: PlayerStepProps) => { - const [player1, setPlayer1] = useState(initialValue); - const [error, setError] = useState(null); - const [filteredNames, setFilteredNames] = useState(playerNameHistory); - const [isModalOpen, setIsModalOpen] = useState(false); - const inputRef = useRef(null); - - useEffect(() => { - if (!player1) { - setFilteredNames(playerNameHistory); - } else { - setFilteredNames( - playerNameHistory.filter(name => - name.toLowerCase().includes(player1.toLowerCase()) - ) - ); - } - }, [player1, playerNameHistory]); - - const handleSubmit = (e: Event) => { - e.preventDefault(); - const trimmedName = player1.trim(); - if (!trimmedName) { - setError(ERROR_MESSAGES.PLAYER1_REQUIRED); - if (inputRef.current) { - inputRef.current.focus(); - inputRef.current.setAttribute('aria-invalid', 'true'); - } - return; - } - if (trimmedName.length > FORM_CONFIG.MAX_PLAYER_NAME_LENGTH) { - setError(`Spielername darf maximal ${FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen lang sein`); - if (inputRef.current) { - inputRef.current.focus(); - inputRef.current.setAttribute('aria-invalid', 'true'); - } - return; - } - setError(null); - if (inputRef.current) { - inputRef.current.setAttribute('aria-invalid', 'false'); - } - onNext(trimmedName); - }; - - const handleQuickPick = (name: string) => { - setError(null); - onNext(name); - }; - - const handleModalSelect = (name: string) => { - setIsModalOpen(false); - handleQuickPick(name); - }; - - const handleClear = () => { - setPlayer1(''); - setError(null); - if (inputRef.current) inputRef.current.focus(); - }; - - return ( -
-
-
Name Spieler 1
- -
- -
-
- -
- { - const target = e.target as HTMLInputElement; - const value = target.value; - setPlayer1(value); - if (value.length > FORM_CONFIG.MAX_PLAYER_NAME_LENGTH) { - setError(`Spielername darf maximal ${FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen lang sein`); - target.setAttribute('aria-invalid', 'true'); - } else if (value.trim() && error) { - setError(null); - target.setAttribute('aria-invalid', 'false'); - } - }} - autoComplete="off" - autoCapitalize="words" - spellCheck={false} - enterKeyHint="next" - aria-label="Name Spieler 1" - aria-describedby="player1-help" - onFocus={(e) => { - const target = e.currentTarget as HTMLInputElement; - target.scrollIntoView({ block: 'center', behavior: 'smooth' }); - }} - style={{ - fontSize: UI_CONSTANTS.INPUT_FONT_SIZE, - minHeight: UI_CONSTANTS.INPUT_MIN_HEIGHT, - marginTop: 12, - marginBottom: 12, - width: '100%', - paddingRight: UI_CONSTANTS.INPUT_PADDING_RIGHT - }} - ref={inputRef} - /> -
- Geben Sie den Namen für Spieler 1 ein. Maximal {FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen erlaubt. -
- {player1.length > FORM_CONFIG.CHARACTER_COUNT_WARNING_THRESHOLD && ( -
FORM_CONFIG.MAX_PLAYER_NAME_LENGTH ? '#f44336' : '#ff9800', - marginTop: '4px', - textAlign: 'right' - }}> - {player1.length}/{FORM_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen -
- )} - {player1 && ( - - )} -
- {filteredNames.length > 0 && ( -
- {filteredNames.slice(0, UI_CONSTANTS.MAX_QUICK_PICKS).map((name, idx) => ( - - ))} - {playerNameHistory.length > UI_CONSTANTS.MAX_QUICK_PICKS && ( - - )} -
- )} -
- {error && ( -
- ⚠️ - {error} -
- )} -
- -
-
- - -
-
- - {isModalOpen && ( - setIsModalOpen(false)} - /> - )} - - ); -}; - - diff --git a/src/lib/features/new-game/steps/Player2Step.tsx b/src/lib/features/new-game/steps/Player2Step.tsx deleted file mode 100644 index f512a86..0000000 --- a/src/lib/features/new-game/steps/Player2Step.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { h } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import styles from '../NewGame.module.css'; -import { ProgressIndicator } from '../ProgressIndicator'; - -interface PlayerStepProps { - playerNameHistory: string[]; - onNext: (name: string) => void; - onCancel: () => void; - initialValue?: string; -} - -export const Player2Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }: PlayerStepProps) => { - const [player2, setPlayer2] = useState(initialValue); - const [error, setError] = useState(null); - const [filteredNames, setFilteredNames] = useState(playerNameHistory); - const inputRef = useRef(null); - - useEffect(() => { - if (!player2) { - setFilteredNames(playerNameHistory); - } else { - setFilteredNames( - playerNameHistory.filter(name => - name.toLowerCase().includes(player2.toLowerCase()) - ) - ); - } - }, [player2, playerNameHistory]); - - const handleSubmit = (e: Event) => { - e.preventDefault(); - if (!player2.trim()) { - setError('Bitte Namen für Spieler 2 eingeben'); - return; - } - setError(null); - onNext(player2.trim()); - }; - - const handleQuickPick = (name: string) => { - setError(null); - onNext(name); - }; - - const handleClear = () => { - setPlayer2(''); - setError(null); - if (inputRef.current) inputRef.current.focus(); - }; - - return ( -
-
-
Name Spieler 2
- -
- -
-
- -
- { - const target = e.target as HTMLInputElement; - setPlayer2(target.value); - }} - autoComplete="off" - autoCapitalize="words" - spellCheck={false} - enterKeyHint="next" - aria-label="Name Spieler 2" - onFocus={(e) => { - const target = e.currentTarget as HTMLInputElement; - target.scrollIntoView({ block: 'center', behavior: 'smooth' }); - }} - style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }} - ref={inputRef} - /> - {player2 && ( - - )} -
- {filteredNames.length > 0 && ( -
- {filteredNames.slice(0, 10).map((name, idx) => ( - - ))} -
- )} -
- {error &&
{error}
} -
- -
-
- - -
-
-
- ); -}; - - diff --git a/src/lib/features/new-game/steps/Player3Step.tsx b/src/lib/features/new-game/steps/Player3Step.tsx deleted file mode 100644 index 453590d..0000000 --- a/src/lib/features/new-game/steps/Player3Step.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { h } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; -import styles from '../NewGame.module.css'; -import { ProgressIndicator } from '../ProgressIndicator'; - -interface PlayerStepProps { - playerNameHistory: string[]; - onNext: (name: string) => void; - onCancel: () => void; - initialValue?: string; -} - -export const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }: PlayerStepProps) => { - const [player3, setPlayer3] = useState(initialValue); - const [filteredNames, setFilteredNames] = useState(playerNameHistory); - const inputRef = useRef(null); - - useEffect(() => { - if (!player3) { - setFilteredNames(playerNameHistory); - } else { - setFilteredNames( - playerNameHistory.filter(name => - name.toLowerCase().includes(player3.toLowerCase()) - ) - ); - } - }, [player3, playerNameHistory]); - - const handleSubmit = (e: Event) => { - e.preventDefault(); - onNext(player3.trim()); - }; - - const handleQuickPick = (name: string) => { - onNext(name); - }; - - const handleClear = () => { - setPlayer3(''); - if (inputRef.current) inputRef.current.focus(); - }; - - const handleSkip = (e: Event) => { - e.preventDefault(); - onNext(''); - }; - - return ( -
-
-
Name Spieler 3 (optional)
- -
- -
-
- -
- { - const target = e.target as HTMLInputElement; - setPlayer3(target.value); - }} - autoComplete="off" - autoCapitalize="words" - spellCheck={false} - enterKeyHint="done" - aria-label="Name Spieler 3" - onFocus={(e) => { - const target = e.currentTarget as HTMLInputElement; - target.scrollIntoView({ block: 'center', behavior: 'smooth' }); - }} - style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }} - ref={inputRef} - /> - {player3 && ( - - )} -
- {filteredNames.length > 0 && ( -
- {filteredNames.slice(0, 10).map((name, idx) => ( - - ))} -
- )} -
-
- -
-
- -
- - -
-
-
-
- ); -}; - - diff --git a/src/lib/features/new-game/steps/RaceToStep.tsx b/src/lib/features/new-game/steps/RaceToStep.tsx index 3e36aa4..aa34cfc 100644 --- a/src/lib/features/new-game/steps/RaceToStep.tsx +++ b/src/lib/features/new-game/steps/RaceToStep.tsx @@ -1,12 +1,13 @@ import { h } from 'preact'; import { useEffect, useState } from 'preact/hooks'; import styles from '../NewGame.module.css'; -import { ProgressIndicator } from '../ProgressIndicator'; import { RACE_TO_QUICK_PICKS, RACE_TO_DEFAULT, RACE_TO_INFINITY, } from '@lib/domain/constants'; +import { WizardNav } from '../components/WizardNav'; +import { WizardStepForm } from '../components/WizardStepForm'; interface RaceToStepProps { onNext: (raceTo: string | number) => void; @@ -47,9 +48,13 @@ export const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: Ra }; return ( -
-
Race To auswählen
- + } + >
-
- - -
- +
); };