refactor(new-game): extract GameTypeStep and RaceToStep

Refs #30

- Add GameTypeStep.tsx and RaceToStep.tsx under src/components/new-game
- Replace inline components with imports in NewGame.tsx
- Pure refactor; behavior unchanged
This commit is contained in:
Frank Schwenk
2025-10-30 15:17:35 +01:00
parent af0ffe8517
commit d6ea0125df
3 changed files with 200 additions and 186 deletions

View File

@@ -18,6 +18,8 @@ import {
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 type { BreakRule } from '../types/game';
// PlayerSelectModal moved to ./new-game/PlayerSelectModal
@@ -41,86 +43,6 @@ interface GameTypeStepProps {
initialValue?: string;
}
/**
* Game Type selection step for multi-step game creation wizard.
*/
const GameTypeStep = ({ onNext, onCancel, initialValue = '' }: GameTypeStepProps) => {
const [gameType, setGameType] = useState(initialValue);
const gameTypes = ['8-Ball', '9-Ball', '10-Ball'];
const handleSelect = (selectedType: string) => {
setGameType(selectedType);
// Auto-advance to next step on selection
onNext(selectedType);
};
const handleSubmit = (e: Event) => {
e.preventDefault();
if (gameType) {
onNext(gameType);
}
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spielart auswählen">
<div className={styles['screen-title']}>Spielart auswählen</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div>
<div className={styles['game-type-selection']}>
{gameTypes.map(type => (
<button
key={type}
type="button"
className={`${styles['game-type-btn']} ${gameType === type ? styles.selected : ''}`}
onClick={() => handleSelect(type)}
>
{type}
</button>
))}
</div>
<div className={styles['arrow-nav']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 48 }}>
<button
type="button"
className={styles['arrow-btn']}
aria-label="Zurück"
onClick={onCancel}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer' }}
>
{/* Unicode left arrow */}
&#8592;
</button>
<button
type="submit"
className={styles['arrow-btn']}
aria-label="Weiter"
disabled={!gameType}
style={{
fontSize: 48,
width: 80,
height: 80,
borderRadius: '50%',
background: '#222',
color: '#fff',
border: 'none',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
cursor: 'pointer',
opacity: !gameType ? 0.5 : 1,
}}
>
{/* Unicode right arrow */}
&#8594;
</button>
</div>
</form>
);
};
interface RaceToStepProps {
onNext: (raceTo: string | number) => void;
@@ -129,112 +51,7 @@ interface RaceToStepProps {
gameType?: string;
}
/**
* Race To selection step for multi-step game creation wizard.
*/
const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToStepProps) => {
const quickPicks = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const defaultValue = 5;
const [raceTo, setRaceTo] = useState<string | number>(initialValue !== '' ? initialValue : defaultValue);
useEffect(() => {
if ((initialValue === '' || initialValue === undefined) && raceTo !== defaultValue) {
setRaceTo(defaultValue);
}
if (initialValue !== '' && initialValue !== undefined && initialValue !== raceTo) {
setRaceTo(initialValue);
}
}, [gameType, initialValue, defaultValue]);
const handleQuickPick = (value: number) => {
// For endlos (endless) games, use Infinity to prevent automatic completion
const selected = value === 0 ? 'Infinity' : value;
setRaceTo(selected);
// Auto-advance to the next step (finalize) when a quick pick is chosen
const raceToValue = selected === 'Infinity' ? Infinity : (parseInt(String(selected), 10) || 0);
onNext(raceToValue);
};
const handleInputChange = (e: Event) => {
const target = e.target as HTMLInputElement;
setRaceTo(target.value);
};
const handleSubmit = (e: Event) => {
e.preventDefault();
// Handle Infinity for endlos games, otherwise parse as integer
const raceToValue = raceTo === 'Infinity' ? Infinity : (parseInt(String(raceTo), 10) || 0);
onNext(raceToValue);
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Race To auswählen">
<div className={styles['screen-title']}>Race To auswählen</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div>
<div className={styles['endlos-container']}>
<button
type="button"
className={`${styles['race-to-btn']} ${styles['endlos-btn']} ${raceTo === 'Infinity' ? styles.selected : ''}`}
onClick={() => handleQuickPick(0)}
>
Endlos
</button>
</div>
<div className={styles['race-to-selection']}>
{quickPicks.map(value => (
<button
key={value}
type="button"
className={`${styles['race-to-btn']} ${parseInt(String(raceTo), 10) === value ? styles.selected : ''}`}
onClick={() => handleQuickPick(value)}
>
{value}
</button>
))}
</div>
<div className={styles['custom-race-to']}>
<input
type="number"
pattern="[0-9]*"
value={raceTo}
onInput={handleInputChange}
className={styles['name-input']}
placeholder="manuelle Eingabe"
/>
</div>
<div className={styles['arrow-nav']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 48 }}>
<button
type="button"
className={styles['arrow-btn']}
aria-label="Zurück"
onClick={onCancel}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer' }}
>
{/* Unicode left arrow */}
&#8592;
</button>
<button
type="submit"
className={styles['arrow-btn']}
aria-label="Weiter"
disabled={String(raceTo).trim() === ''}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer', opacity: String(raceTo).trim() === '' ? 0.5 : 1 }}
>
{/* Unicode right arrow */}
&#8594;
</button>
</div>
</form>
);
};
// GameTypeStep and RaceToStep moved to ./new-game
interface BreakRuleStepProps {
onNext: (rule: BreakRule) => void;

View File

@@ -0,0 +1,86 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import styles from '../NewGame.module.css';
interface GameTypeStepProps {
onNext: (type: string) => void;
onCancel: () => void;
initialValue?: string;
}
export const GameTypeStep = ({ onNext, onCancel, initialValue = '' }: GameTypeStepProps) => {
const [gameType, setGameType] = useState(initialValue);
const gameTypes = ['8-Ball', '9-Ball', '10-Ball'];
const handleSelect = (selectedType: string) => {
setGameType(selectedType);
onNext(selectedType);
};
const handleSubmit = (e: Event) => {
e.preventDefault();
if (gameType) {
onNext(gameType);
}
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spielart auswählen">
<div className={styles['screen-title']}>Spielart auswählen</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div>
<div className={styles['game-type-selection']}>
{gameTypes.map(type => (
<button
key={type}
type="button"
className={`${styles['game-type-btn']} ${gameType === type ? styles.selected : ''}`}
onClick={() => handleSelect(type)}
>
{type}
</button>
))}
</div>
<div className={styles['arrow-nav']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 48 }}>
<button
type="button"
className={styles['arrow-btn']}
aria-label="Zurück"
onClick={onCancel}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer' }}
>
&#8592;
</button>
<button
type="submit"
className={styles['arrow-btn']}
aria-label="Weiter"
disabled={!gameType}
style={{
fontSize: 48,
width: 80,
height: 80,
borderRadius: '50%',
background: '#222',
color: '#fff',
border: 'none',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
cursor: 'pointer',
opacity: !gameType ? 0.5 : 1,
}}
>
&#8594;
</button>
</div>
</form>
);
};

View File

@@ -0,0 +1,111 @@
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import styles from '../NewGame.module.css';
interface RaceToStepProps {
onNext: (raceTo: string | number) => void;
onCancel: () => void;
initialValue?: string | number;
gameType?: string;
}
export const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToStepProps) => {
const quickPicks = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const defaultValue = 5;
const [raceTo, setRaceTo] = useState<string | number>(initialValue !== '' ? initialValue : defaultValue);
useEffect(() => {
if ((initialValue === '' || initialValue === undefined) && raceTo !== defaultValue) {
setRaceTo(defaultValue);
}
if (initialValue !== '' && initialValue !== undefined && initialValue !== raceTo) {
setRaceTo(initialValue);
}
}, [gameType, initialValue, defaultValue]);
const handleQuickPick = (value: number) => {
const selected = value === 0 ? 'Infinity' : value;
setRaceTo(selected);
const raceToValue = selected === 'Infinity' ? Infinity : (parseInt(String(selected), 10) || 0);
onNext(raceToValue);
};
const handleInputChange = (e: Event) => {
const target = e.target as HTMLInputElement;
setRaceTo(target.value);
};
const handleSubmit = (e: Event) => {
e.preventDefault();
const raceToValue = raceTo === 'Infinity' ? Infinity : (parseInt(String(raceTo), 10) || 0);
onNext(raceToValue);
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Race To auswählen">
<div className={styles['screen-title']}>Race To auswählen</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div>
<div className={styles['endlos-container']}>
<button
type="button"
className={`${styles['race-to-btn']} ${styles['endlos-btn']} ${raceTo === 'Infinity' ? styles.selected : ''}`}
onClick={() => handleQuickPick(0)}
>
Endlos
</button>
</div>
<div className={styles['race-to-selection']}>
{quickPicks.map(value => (
<button
key={value}
type="button"
className={`${styles['race-to-btn']} ${parseInt(String(raceTo), 10) === value ? styles.selected : ''}`}
onClick={() => handleQuickPick(value)}
>
{value}
</button>
))}
</div>
<div className={styles['custom-race-to']}>
<input
type="number"
pattern="[0-9]*"
value={raceTo}
onInput={handleInputChange}
className={styles['name-input']}
placeholder="manuelle Eingabe"
/>
</div>
<div className={styles['arrow-nav']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 48 }}>
<button
type="button"
className={styles['arrow-btn']}
aria-label="Zurück"
onClick={onCancel}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer' }}
>
&#8592;
</button>
<button
type="submit"
className={styles['arrow-btn']}
aria-label="Weiter"
disabled={String(raceTo).trim() === ''}
style={{ fontSize: 48, width: 80, height: 80, borderRadius: '50%', background: '#222', color: '#fff', border: 'none', boxShadow: '0 2px 8px rgba(0,0,0,0.15)', cursor: 'pointer', opacity: String(raceTo).trim() === '' ? 0.5 : 1 }}
>
&#8594;
</button>
</div>
</form>
);
};