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:
@@ -18,6 +18,8 @@ import {
|
|||||||
import { Player1Step } from './new-game/Player1Step';
|
import { Player1Step } from './new-game/Player1Step';
|
||||||
import { Player2Step } from './new-game/Player2Step';
|
import { Player2Step } from './new-game/Player2Step';
|
||||||
import { Player3Step } from './new-game/Player3Step';
|
import { Player3Step } from './new-game/Player3Step';
|
||||||
|
import { GameTypeStep } from './new-game/GameTypeStep';
|
||||||
|
import { RaceToStep } from './new-game/RaceToStep';
|
||||||
import type { BreakRule } from '../types/game';
|
import type { BreakRule } from '../types/game';
|
||||||
|
|
||||||
// PlayerSelectModal moved to ./new-game/PlayerSelectModal
|
// PlayerSelectModal moved to ./new-game/PlayerSelectModal
|
||||||
@@ -41,86 +43,6 @@ interface GameTypeStepProps {
|
|||||||
initialValue?: string;
|
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 */}
|
|
||||||
←
|
|
||||||
</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 */}
|
|
||||||
→
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface RaceToStepProps {
|
interface RaceToStepProps {
|
||||||
onNext: (raceTo: string | number) => void;
|
onNext: (raceTo: string | number) => void;
|
||||||
@@ -129,112 +51,7 @@ interface RaceToStepProps {
|
|||||||
gameType?: string;
|
gameType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// GameTypeStep and RaceToStep moved to ./new-game
|
||||||
* 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 */}
|
|
||||||
←
|
|
||||||
</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 */}
|
|
||||||
→
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BreakRuleStepProps {
|
interface BreakRuleStepProps {
|
||||||
onNext: (rule: BreakRule) => void;
|
onNext: (rule: BreakRule) => void;
|
||||||
|
|||||||
86
src/components/new-game/GameTypeStep.tsx
Normal file
86
src/components/new-game/GameTypeStep.tsx
Normal 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' }}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
111
src/components/new-game/RaceToStep.tsx
Normal file
111
src/components/new-game/RaceToStep.tsx
Normal 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' }}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</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 }}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user