From f8b461e189b03c85360ec5aef37b48f5701ad839 Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Thu, 30 Oct 2025 15:09:02 +0100 Subject: [PATCH] refactor(new-game): extract Player1Step into separate module Refs #30 - Add src/components/new-game/Player1Step.tsx - Replace inline Player1Step with import in NewGame.tsx - Preserve behavior; no UI or logic changes --- src/components/NewGame.tsx | 256 +---------------------- src/components/new-game/Player1Step.tsx | 258 ++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 254 deletions(-) create mode 100644 src/components/new-game/Player1Step.tsx diff --git a/src/components/NewGame.tsx b/src/components/NewGame.tsx index c5b2ad9..febbaba 100644 --- a/src/components/NewGame.tsx +++ b/src/components/NewGame.tsx @@ -15,6 +15,7 @@ import { FORM_CONFIG, ERROR_STYLES } from '../utils/constants'; +import { Player1Step } from './new-game/Player1Step'; import type { BreakRule } from '../types/game'; // PlayerSelectModal moved to ./new-game/PlayerSelectModal @@ -26,260 +27,7 @@ interface PlayerStepProps { initialValue?: string; } -/** - * Player 1 input step for multi-step game creation wizard. - */ -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(() => { - const el = inputRef.current; - if (el) { - el.focus(); - const end = el.value.length; - try { - el.setSelectionRange(end, end); - } catch {} - } - }, []); - - 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); - - // Real-time validation feedback - 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" - aria-label="Name Spieler 1" - aria-describedby="player1-help" - 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)} - /> - )} -
- - -
- - ); -}; +// Player1Step moved to ./new-game/Player1Step /** * Player 2 input step for multi-step game creation wizard. diff --git a/src/components/new-game/Player1Step.tsx b/src/components/new-game/Player1Step.tsx new file mode 100644 index 0000000..0230dec --- /dev/null +++ b/src/components/new-game/Player1Step.tsx @@ -0,0 +1,258 @@ +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 '../../utils/constants'; +import { PlayerSelectModal } from './PlayerSelectModal'; + +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(() => { + const el = inputRef.current; + if (el) { + el.focus(); + const end = el.value.length; + try { + el.setSelectionRange(end, end); + } catch {} + } + }, []); + + 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" + aria-label="Name Spieler 1" + aria-describedby="player1-help" + 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)} + /> + )} +
+ + +
+ + ); +}; + +