Compare commits

9 Commits

Author SHA1 Message Date
Frank Schwenk
875e9c8795 feat: Implement rematch functionality
Refs #17

Adds a 'Rematch' button to the game completion modal.

Introduces a 'handleRematch' function in 'App.jsx' to create a new game with the same players and settings as the previous game.

The 'onRematch' handler is passed down to the 'GameCompletionModal' component.

Adds '.gitea' to '.gitignore' to prevent tracking local issue context.
2025-06-20 13:35:42 +02:00
Frank Schwenk
429d479f69 feat: Implement score increment on click and cleanup CSS
- Implemented score increment functionality directly on the score display in GameDetail.jsx.

- Deleted obsolete ValidationModal.module.css to finalize style consolidation.

- This resolves all outstanding tasks for the refactor and adds a minor feature enhancement.

- Closes #1
2025-06-20 13:01:30 +02:00
Frank Schwenk
dbc173f57b feat: optimize player selection for touch input
- Increases the number of quick-pick buttons from 4 to 10.
- Adds a '...' button that appears when more than 10 players exist in history.
- Clicking '...' opens a scrollable modal listing all past players for easy selection.
- This provides a much faster player selection flow on touch devices.

Closes #4
2025-06-20 11:03:37 +02:00
Frank Schwenk
b466dd2a0a feat: complete wizard navigation for all steps
- Adds forward navigation arrows to the 'Game Type' and 'Race To' steps in the new game wizard.
- Unifies navigation logic across all five steps.
- Users can now review their selections before proceeding.

Closes #11
2025-06-20 10:44:27 +02:00
Frank Schwenk
14fd711858 refactor: Update Race To step UI and logic
- Renames 'Offen' to 'Endlos' and moves it to a separate line.
- Extends quick-pick buttons to include 1-9.
- Removes 'Custom' button and makes numeric input always visible.
- Updates placeholder text for the custom input.
- Closes #10
2025-06-20 10:23:14 +02:00
Frank Schwenk
1c77661dbc feat: Implement Game Type selection step
- Adds the 'GameTypeStep' to the new game wizard.
- Features large, touch-friendly buttons for selecting game types.
- Selection automatically proceeds to the next step.
- Includes progress indicator and back navigation.
- Closes #9
2025-06-20 10:16:13 +02:00
Frank Schwenk
47554cdd27 feat: Implement Player 3 input step
- Implemented the third step of the new game creation wizard for Player 3's name input.
- The step is optional and includes a 'Skip' button.
- Includes autosuggestions from player history and quick-pick buttons.
- Aligned styling and layout with previous steps, including fixes for button alignment.
- Closes #8
2025-06-20 10:10:32 +02:00
Frank Schwenk
a2b618ce16 feat(wizard): Player 2 step and multi-step navigation
- Add Player2Step component for player 2 name input
- Implement multi-step wizard logic in App.jsx
- Player 1/2 steps now allow forward/back navigation
- Left arrow on Player 2 returns to Player 1, not abort
- Prepare placeholders for further steps

Closes #7
2025-06-18 20:59:05 +02:00
Frank Schwenk
76ef005cda feat(wizard): Player 1 step UI overhaul
- Quick-pick now advances to next step
- Added clear (×) icon to input field
- Replaced navigation buttons with large left/right arrows
- All controls are touch-friendly and visually prominent

Closes #6
2025-06-18 20:48:28 +02:00
9 changed files with 953 additions and 99 deletions

2
.gitea
View File

@@ -1 +1 @@
@https://gitea.schwenk.online/froxxxy/bscscore/issues/1
@https://gitea.schwenk.online/froxxxy/bscscore/issues/10

2
.gitignore vendored
View File

@@ -24,4 +24,4 @@ pnpm-debug.log*
.idea/
.gitea
dev/
dev/.gitea

View File

@@ -2,7 +2,7 @@ import { h } from 'preact';
import { useState, useEffect, useCallback } from 'preact/hooks';
import GameList from './GameList.jsx';
import GameDetail from './GameDetail.jsx';
import NewGame from './NewGame.jsx';
import { Player1Step, Player2Step, Player3Step, GameTypeStep, RaceToStep } from './NewGame.jsx';
import Modal from './Modal.jsx';
import ValidationModal from './ValidationModal.jsx';
import GameCompletionModal from './GameCompletionModal.jsx';
@@ -23,6 +23,8 @@ const App = () => {
const [validation, setValidation] = useState({ open: false, message: '' });
const [completionModal, setCompletionModal] = useState({ open: false, game: null });
const [filter, setFilter] = useState('all');
const [newGameStep, setNewGameStep] = useState(null);
const [newGameData, setNewGameData] = useState({ player1: '', player2: '', player3: '', gameType: '', raceTo: '' });
// Load games from localStorage on mount
useEffect(() => {
@@ -55,6 +57,8 @@ const App = () => {
const showNewGame = useCallback(() => {
setScreen('new-game');
setCurrentGameId(null);
setNewGameStep('player1');
setNewGameData({ player1: '', player2: '', player3: '', gameType: '', raceTo: '' });
}, []);
const showGameDetail = useCallback((id) => {
setCurrentGameId(id);
@@ -113,6 +117,22 @@ const App = () => {
setScreen('game-detail');
}, [currentGameId]);
const handleRematch = useCallback(() => {
const completedGame = games.find(g => g.id === currentGameId);
if (!completedGame) return;
const newId = handleCreateGame({
player1: completedGame.player1,
player2: completedGame.player2,
player3: completedGame.player3,
gameType: completedGame.gameType,
raceTo: completedGame.raceTo,
});
setCompletionModal({ open: false, game: null });
showGameDetail(newId);
}, [games, currentGameId, handleCreateGame, showGameDetail]);
// Delete game
const handleDeleteGame = useCallback((id) => {
setModal({ open: true, gameId: id });
@@ -134,6 +154,37 @@ const App = () => {
setValidation({ open: false, message: '' });
}, []);
// Step handlers
const handlePlayer1Next = (name) => {
setNewGameData(data => ({ ...data, player1: name }));
setNewGameStep('player2');
};
const handlePlayer2Next = (name) => {
setNewGameData(data => ({ ...data, player2: name }));
setNewGameStep('player3');
};
const handlePlayer3Next = (name) => {
setNewGameData(data => ({ ...data, player3: name }));
setNewGameStep('gameType');
};
const handleGameTypeNext = (type) => {
setNewGameData(data => ({ ...data, gameType: type }));
setNewGameStep('raceTo');
};
const handleRaceToNext = (raceTo) => {
const finalData = { ...newGameData, raceTo };
const newId = handleCreateGame(finalData);
showGameDetail(newId);
setNewGameStep(null);
setNewGameData({ player1: '', player2: '', player3: '', gameType: '', raceTo: '' });
};
const handleCancelNewGame = useCallback(() => {
setScreen('game-list');
setNewGameStep(null);
setNewGameData({ player1: '', player2: '', player3: '', gameType: '', raceTo: '' });
}, []);
return (
<div className="screen-container">
{screen === 'game-list' && (
@@ -153,16 +204,44 @@ const App = () => {
{screen === 'new-game' && (
<div className="screen active">
<div className="screen-content">
<NewGame
onCreateGame={handleCreateGame}
playerNameHistory={playerNameHistory}
onCancel={showGameList}
onGameCreated={id => {
setCurrentGameId(id);
setScreen('game-detail');
}}
initialValues={games[0]}
/>
{newGameStep === 'player1' && (
<Player1Step
playerNameHistory={playerNameHistory}
onNext={handlePlayer1Next}
onCancel={handleCancelNewGame}
initialValue={newGameData.player1}
/>
)}
{newGameStep === 'player2' && (
<Player2Step
playerNameHistory={playerNameHistory}
onNext={handlePlayer2Next}
onCancel={() => setNewGameStep('player1')}
initialValue={newGameData.player2}
/>
)}
{newGameStep === 'player3' && (
<Player3Step
playerNameHistory={playerNameHistory}
onNext={handlePlayer3Next}
onCancel={() => setNewGameStep('player2')}
initialValue={newGameData.player3}
/>
)}
{newGameStep === 'gameType' && (
<GameTypeStep
onNext={handleGameTypeNext}
onCancel={() => setNewGameStep('player3')}
initialValue={newGameData.gameType}
/>
)}
{newGameStep === 'raceTo' && (
<RaceToStep
onNext={handleRaceToNext}
onCancel={() => setNewGameStep('gameType')}
initialValue={newGameData.raceTo}
/>
)}
</div>
</div>
)}
@@ -195,6 +274,7 @@ const App = () => {
game={completionModal.game}
onConfirm={handleConfirmCompletion}
onClose={() => setCompletionModal({ open: false, game: null })}
onRematch={handleRematch}
/>
<FullscreenToggle />
</div>

View File

@@ -9,9 +9,10 @@ import styles from './GameCompletionModal.module.css';
* @param {object} props.game
* @param {Function} props.onConfirm
* @param {Function} props.onClose
* @param {Function} props.onRematch
* @returns {import('preact').VNode|null}
*/
const GameCompletionModal = ({ open, game, onConfirm, onClose }) => {
const GameCompletionModal = ({ open, game, onConfirm, onClose, onRematch }) => {
if (!open || !game) return null;
const playerNames = [game.player1, game.player2, game.player3].filter(Boolean);
const scores = [game.score1, game.score2, game.score3].filter((_, i) => playerNames[i]);
@@ -41,6 +42,7 @@ const GameCompletionModal = ({ open, game, onConfirm, onClose }) => {
</div>
<div className={modalStyles['modal-footer']}>
<button className={styles['btn'] + ' ' + styles['btn--warning']} onClick={onConfirm} aria-label="Bestätigen">Bestätigen</button>
<button className={styles['btn'] + ' ' + styles['btn--primary']} onClick={onRematch} aria-label="Rematch">Rematch</button>
<button className={styles['btn']} onClick={onClose} aria-label="Abbrechen">Abbrechen</button>
</div>
</div>

View File

@@ -28,7 +28,14 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onBack }) => {
key={name + idx}
>
<span className={styles['player-name']}>{name}</span>
<span className={styles['score']} id={`score${idx+1}`}>{scores[idx]}</span>
<span
className={styles['score']}
id={`score${idx + 1}`}
onClick={() => !isCompleted && onUpdateScore(idx + 1, 1)}
aria-label={`Aktueller Punktestand für ${name}: ${scores[idx]}. Klicken zum Erhöhen.`}
>
{scores[idx]}
</span>
<button
className={styles['score-button']}
disabled={isCompleted}

View File

@@ -1,117 +1,648 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { useState, useEffect, useRef } from 'preact/hooks';
import styles from './NewGame.module.css';
import modalStyles from './PlayerSelectModal.module.css';
const PlayerSelectModal = ({ players, onSelect, onClose }) => (
<div className={modalStyles.modalOverlay} onClick={onClose}>
<div className={modalStyles.modalContent} onClick={e => e.stopPropagation()}>
<div className={modalStyles.modalHeader}>
<h3>Alle Spieler</h3>
<button className={modalStyles.closeButton} onClick={onClose}>×</button>
</div>
<div className={modalStyles.playerList}>
{players.map(player => (
<button key={player} className={modalStyles.playerItem} onClick={() => onSelect(player)}>
{player}
</button>
))}
</div>
</div>
</div>
);
/**
* New game creation form.
* Player 1 input step for multi-step game creation wizard.
* @param {object} props
* @param {Function} props.onCreateGame
* @param {string[]} props.playerNameHistory
* @param {Function} props.onNext
* @param {Function} props.onCancel
* @param {Function} props.onGameCreated
* @param {object} props.initialValues
* @param {string} [props.initialValue]
* @returns {import('preact').VNode}
*/
const NewGame = ({ onCreateGame, playerNameHistory, onCancel, onGameCreated, initialValues }) => {
const [player1, setPlayer1] = useState(initialValues?.player1 || '');
const [player2, setPlayer2] = useState(initialValues?.player2 || '');
const [player3, setPlayer3] = useState(initialValues?.player3 || '');
const [gameType, setGameType] = useState(initialValues?.gameType || '8-Ball');
const [raceTo, setRaceTo] = useState(initialValues?.raceTo ? String(initialValues.raceTo) : '');
const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }) => {
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(() => {
setPlayer1(initialValues?.player1 || '');
setPlayer2(initialValues?.player2 || '');
setPlayer3(initialValues?.player3 || '');
setGameType(initialValues?.gameType || '8-Ball');
setRaceTo(initialValues?.raceTo ? String(initialValues.raceTo) : '');
setError(null);
}, [initialValues]);
if (!player1) {
setFilteredNames(playerNameHistory);
} else {
setFilteredNames(
playerNameHistory.filter(name =>
name.toLowerCase().includes(player1.toLowerCase())
)
);
}
}, [player1, playerNameHistory]);
const handleSubmit = (e) => {
e.preventDefault();
if (!player1.trim() || !player2.trim()) {
setError('Bitte Namen für beide Spieler eingeben');
if (!player1.trim()) {
setError('Bitte Namen für Spieler 1 eingeben');
return;
}
const id = onCreateGame({
player1: player1.trim(),
player2: player2.trim(),
player3: player3.trim() || null,
gameType,
raceTo: raceTo ? parseInt(raceTo) : null
});
if (onGameCreated && id) {
onGameCreated(id);
}
setError(null);
onNext(player1.trim());
};
const handleQuickPick = (name) => {
setError(null);
onNext(name);
};
const handleModalSelect = (name) => {
setIsModalOpen(false);
handleQuickPick(name);
};
const handleClear = () => {
setPlayer1('');
setPlayer2('');
setPlayer3('');
setGameType('8-Ball');
setRaceTo('');
setError(null);
if (inputRef.current) inputRef.current.focus();
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Neues Spiel Formular">
<div className={styles['screen-title']}>Neues Spiel</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<button type="button" className="btn" onClick={handleClear} aria-label="Felder leeren">Felder leeren</button>
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 1 Eingabe" autoComplete="off">
<div className={styles['screen-title']}>Neues Spiel Schritt 1/5</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div>
<div className={styles['player-inputs']}>
<div className={styles['player-input']}>
<label htmlFor="player1-input">Spieler 1</label>
<div className={styles['name-input-container']}>
<input id="player1-input" className={styles['name-input']} placeholder="Name Spieler 1" value={player1} onInput={e => setPlayer1(e.target.value)} list="player1-history" aria-label="Name Spieler 1" />
<datalist id="player1-history">
{playerNameHistory.map((name, idx) => <option value={name} key={name + idx} />)}
</datalist>
</div>
<div className={styles['player-input']} style={{ marginBottom: 32, position: 'relative' }}>
<label htmlFor="player1-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 1</label>
<div style={{ position: 'relative', width: '100%' }}>
<input
id="player1-input"
className={styles['name-input']}
placeholder="Name Spieler 1"
value={player1}
onInput={e => setPlayer1(e.target.value)}
autoFocus
autoComplete="off"
aria-label="Name Spieler 1"
style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }}
ref={inputRef}
/>
{player1 && (
<button
type="button"
className={styles['clear-input-btn']}
aria-label="Feld leeren"
onClick={handleClear}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 24,
color: '#aaa',
padding: 0,
zIndex: 2
}}
tabIndex={0}
>
{/* Unicode heavy multiplication X */}
×
</button>
)}
</div>
<div className={styles['player-input']}>
<label htmlFor="player2-input">Spieler 2</label>
<div className={styles['name-input-container']}>
<input id="player2-input" className={styles['name-input']} placeholder="Name Spieler 2" value={player2} onInput={e => setPlayer2(e.target.value)} list="player2-history" aria-label="Name Spieler 2" />
<datalist id="player2-history">
{playerNameHistory.map((name, idx) => <option value={name} key={name + idx} />)}
</datalist>
{filteredNames.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 8 }}>
{filteredNames.slice(0, 10).map((name, idx) => (
<button
type="button"
key={name + idx}
className={styles['quick-pick-btn']}
style={{ fontSize: '1.1rem', padding: '12px 20px', borderRadius: 8, background: '#333', color: '#fff', border: 'none', cursor: 'pointer' }}
onClick={() => handleQuickPick(name)}
aria-label={`Schnellauswahl: ${name}`}
>
{name}
</button>
))}
{playerNameHistory.length > 10 && (
<button
type="button"
className={styles['quick-pick-btn']}
style={{ fontSize: '1.1rem', padding: '12px 20px', borderRadius: 8, background: '#333', color: '#fff', border: 'none', cursor: 'pointer' }}
onClick={() => setIsModalOpen(true)}
aria-label="Weitere Spieler anzeigen"
>
...
</button>
)}
</div>
</div>
<div className={styles['player-input']}>
<label htmlFor="player3-input">Spieler 3 (optional)</label>
<div className={styles['name-input-container']}>
<input id="player3-input" className={styles['name-input']} placeholder="Name Spieler 3" value={player3} onInput={e => setPlayer3(e.target.value)} list="player3-history" aria-label="Name Spieler 3" />
<datalist id="player3-history">
{playerNameHistory.map((name, idx) => <option value={name} key={name + idx} />)}
</datalist>
</div>
</div>
)}
</div>
<div className={styles['game-settings']}>
<div className={styles['setting-group']}>
<label htmlFor="game-type-select">Spieltyp</label>
<select id="game-type-select" value={gameType} onChange={e => setGameType(e.target.value)} aria-label="Spieltyp">
<option value="8-Ball">8-Ball</option>
<option value="9-Ball">9-Ball</option>
<option value="10-Ball">10-Ball</option>
</select>
</div>
<div className={styles['setting-group']}>
<label htmlFor="race-to-input">Race to X (optional)</label>
<input id="race-to-input" type="number" value={raceTo} onInput={e => setRaceTo(e.target.value)} min="1" aria-label="Race to X" />
</div>
</div>
{error && <div className={styles['validation-error']}>{error}</div>}
<div className="nav-buttons">
<button type="button" className="btn" onClick={onCancel} aria-label="Abbrechen">Abbrechen</button>
<button type="submit" className="btn" aria-label="Spiel starten">Spiel starten</button>
{error && <div className={styles['validation-error']} style={{ marginBottom: 16 }}>{error}</div>}
{isModalOpen && (
<PlayerSelectModal
players={playerNameHistory}
onSelect={handleModalSelect}
onClose={() => setIsModalOpen(false)}
/>
)}
<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"
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 right arrow */}
&#8594;
</button>
</div>
</form>
);
};
export default NewGame;
/**
* Player 2 input step for multi-step game creation wizard.
* @param {object} props
* @param {string[]} props.playerNameHistory
* @param {Function} props.onNext
* @param {Function} props.onCancel
* @param {string} [props.initialValue]
* @returns {import('preact').VNode}
*/
const Player2Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }) => {
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) => {
e.preventDefault();
if (!player2.trim()) {
setError('Bitte Namen für Spieler 2 eingeben');
return;
}
setError(null);
onNext(player2.trim());
};
const handleQuickPick = (name) => {
setError(null);
onNext(name);
};
const handleClear = () => {
setPlayer2('');
setError(null);
if (inputRef.current) inputRef.current.focus();
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 2 Eingabe" autoComplete="off">
<div className={styles['screen-title']}>Neues Spiel Schritt 2/5</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<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['player-input']} style={{ marginBottom: 32, position: 'relative' }}>
<label htmlFor="player2-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 2</label>
<div style={{ position: 'relative', width: '100%' }}>
<input
id="player2-input"
className={styles['name-input']}
placeholder="Name Spieler 2"
value={player2}
onInput={e => setPlayer2(e.target.value)}
autoFocus
autoComplete="off"
aria-label="Name Spieler 2"
style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }}
ref={inputRef}
/>
{player2 && (
<button
type="button"
className={styles['clear-input-btn']}
aria-label="Feld leeren"
onClick={handleClear}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 24,
color: '#aaa',
padding: 0,
zIndex: 2
}}
tabIndex={0}
>
×
</button>
)}
</div>
{filteredNames.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 8 }}>
{filteredNames.slice(0, 10).map((name, idx) => (
<button
type="button"
key={name + idx}
className={styles['quick-pick-btn']}
style={{ fontSize: '1.1rem', padding: '12px 20px', borderRadius: 8, background: '#333', color: '#fff', border: 'none', cursor: 'pointer' }}
onClick={() => handleQuickPick(name)}
aria-label={`Schnellauswahl: ${name}`}
>
{name}
</button>
))}
</div>
)}
</div>
{error && <div className={styles['validation-error']} style={{ marginBottom: 16 }}>{error}</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"
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' }}
>
&#8594;
</button>
</div>
</form>
);
};
/**
* Player 3 input step for multi-step game creation wizard.
* @param {object} props
* @param {string[]} props.playerNameHistory
* @param {Function} props.onNext
* @param {Function} props.onCancel
* @param {string} [props.initialValue]
* @returns {import('preact').VNode}
*/
const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }) => {
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) => {
e.preventDefault();
// Player 3 is optional, so always allow submission
onNext(player3.trim());
};
const handleQuickPick = (name) => {
onNext(name);
};
const handleClear = () => {
setPlayer3('');
if (inputRef.current) inputRef.current.focus();
};
const handleSkip = (e) => {
e.preventDefault();
onNext('');
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 3 Eingabe" autoComplete="off">
<div className={styles['screen-title']}>Neues Spiel Schritt 3/5</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}>
<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['player-input']} style={{ marginBottom: 32, position: 'relative' }}>
<label htmlFor="player3-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 3 (optional)</label>
<div style={{ position: 'relative', width: '100%' }}>
<input
id="player3-input"
className={styles['name-input']}
placeholder="Name Spieler 3 (optional)"
value={player3}
onInput={e => setPlayer3(e.target.value)}
autoFocus
autoComplete="off"
aria-label="Name Spieler 3"
style={{ fontSize: '1.2rem', minHeight: 48, marginTop: 12, marginBottom: 12, width: '100%', paddingRight: 44 }}
ref={inputRef}
/>
{player3 && (
<button
type="button"
className={styles['clear-input-btn']}
aria-label="Feld leeren"
onClick={handleClear}
style={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: 24,
color: '#aaa',
padding: 0,
zIndex: 2
}}
tabIndex={0}
>
×
</button>
)}
</div>
{filteredNames.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 8 }}>
{filteredNames.slice(0, 10).map((name, idx) => (
<button
type="button"
key={name + idx}
className={styles['quick-pick-btn']}
style={{ fontSize: '1.1rem', padding: '12px 20px', borderRadius: 8, background: '#333', color: '#fff', border: 'none', cursor: 'pointer' }}
onClick={() => handleQuickPick(name)}
aria-label={`Schnellauswahl: ${name}`}
>
{name}
</button>
))}
</div>
)}
</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>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<button
type="button"
onClick={handleSkip}
className={styles['quick-pick-btn']}
style={{ fontSize: '1.1rem', padding: '12px 20px', borderRadius: 8, background: '#333', color: '#fff', border: 'none', cursor: 'pointer' }}
>
Überspringen
</button>
<button
type="submit"
className={styles['arrow-btn']}
aria-label="Weiter"
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' }}
>
&#8594;
</button>
</div>
</div>
</form>
);
};
/**
* Game Type selection step for multi-step game creation wizard.
* @param {object} props
* @param {Function} props.onNext
* @param {Function} props.onCancel
* @param {string} [props.initialValue]
* @returns {import('preact').VNode}
*/
const GameTypeStep = ({ onNext, onCancel, initialValue = '' }) => {
const [gameType, setGameType] = useState(initialValue);
const gameTypes = ['8-Ball', '9-Ball', '10-Ball', '14/1 endlos'];
const handleSelect = (selectedType) => {
setGameType(selectedType);
};
const handleSubmit = (e) => {
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']}>Neues Spiel Schritt 4/5</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']} />
</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>
);
};
/**
* Race To selection step for multi-step game creation wizard.
* @param {object} props
* @param {Function} props.onNext
* @param {Function} props.onCancel
* @param {string|number} [props.initialValue]
* @returns {import('preact').VNode}
*/
const RaceToStep = ({ onNext, onCancel, initialValue = '' }) => {
const [raceTo, setRaceTo] = useState(initialValue);
const quickPicks = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const handleQuickPick = (value) => {
setRaceTo(value);
};
const handleInputChange = (e) => {
setRaceTo(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
onNext(parseInt(raceTo, 10) || 0);
};
return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Race To auswählen">
<div className={styles['screen-title']}>Neues Spiel Schritt 5/5</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']} />
</div>
<div className={styles['endlos-container']}>
<button
type="button"
className={`${styles['race-to-btn']} ${styles['endlos-btn']} ${raceTo === 0 ? 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(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="Fertigstellen"
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 checkmark */}
&#10003;
</button>
</div>
</form>
);
};
export { Player1Step, Player2Step, Player3Step, GameTypeStep, RaceToStep };

View File

@@ -109,4 +109,169 @@
display: flex;
flex-direction: column;
gap: 0;
}
.progress-indicator {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.progress-dot {
width: 14px;
height: 14px;
border-radius: 50%;
background: #444;
opacity: 0.4;
transition: background 0.2s, opacity 0.2s;
}
.progress-dot.active {
background: #fff;
opacity: 1;
}
.quick-pick-btn {
min-width: 80px;
min-height: 44px;
font-size: 1.1rem;
border-radius: 8px;
background: #333;
color: #fff;
border: none;
cursor: pointer;
margin-bottom: 8px;
transition: background 0.2s;
}
.quick-pick-btn:active, .quick-pick-btn:focus {
background: #555;
outline: none;
}
.arrow-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 48px;
width: 100%;
}
.arrow-btn {
font-size: 48px;
width: 80px;
height: 80px;
border-radius: 50%;
background: #222;
color: #fff;
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, color 0.2s;
}
.arrow-btn:active, .arrow-btn:focus {
background: #444;
color: #fff;
outline: none;
}
.clear-input-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 24px;
color: #aaa;
padding: 0;
z-index: 2;
}
.clear-input-btn:active, .clear-input-btn:focus {
color: #fff;
outline: none;
}
.game-type-selection {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
width: 100%;
margin: 16px 0;
}
.game-type-btn {
background: #2a2a2a;
border: 2px solid #333;
color: #fff;
font-size: 1.4rem;
font-weight: 600;
padding: 24px;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: background 0.2s, border-color 0.2s;
}
.game-type-btn:hover {
background: #333;
border-color: #555;
}
.game-type-btn.selected {
background: #4a4a4a;
border-color: #777;
}
.race-to-selection {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
gap: 12px;
width: 100%;
margin: 16px 0;
}
.race-to-btn {
background: #2a2a2a;
border: 2px solid #333;
color: #fff;
font-size: 1.3rem;
font-weight: 600;
padding: 16px 8px;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: background 0.2s, border-color 0.2s;
min-height: 70px;
}
.race-to-btn:hover {
background: #333;
border-color: #555;
}
.race-to-btn.selected {
background: #4a4a4a;
border-color: #777;
}
.custom-race-to {
display: flex;
gap: 12px;
margin-top: 24px;
align-items: center;
}
.custom-race-to input {
flex-grow: 1;
}
.custom-race-to .arrow-btn {
width: 60px;
height: 60px;
font-size: 32px;
flex-shrink: 0;
}
.endlos-container {
width: 100%;
margin-bottom: 12px;
}
.endlos-btn {
width: 100%;
}

View File

@@ -0,0 +1,70 @@
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modalContent {
background: #2c2c2c;
padding: 24px;
border-radius: 12px;
width: 90%;
max-width: 400px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
}
.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modalHeader h3 {
margin: 0;
font-size: 1.5rem;
color: #fff;
}
.closeButton {
background: none;
border: none;
font-size: 2rem;
color: #aaa;
cursor: pointer;
padding: 0;
line-height: 1;
}
.playerList {
max-height: 60vh;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.playerItem {
background: #444;
color: #fff;
border: none;
padding: 16px;
border-radius: 8px;
text-align: left;
font-size: 1.2rem;
cursor: pointer;
transition: background-color 0.2s;
}
.playerItem:hover {
background: #555;
}

View File

@@ -1 +0,0 @@
/* DEPRECATED: All modal styles are now in Modal.module.css */