Compare commits

16 Commits

Author SHA1 Message Date
Frank Schwenk
6da7a5f4e2 Merge feature/winnerbreak-wechselbreak-#28 into main (Refs #28) 2025-10-30 14:07:38 +01:00
Frank Schwenk
d22bbdb3dc fix(#28): preselect Winnerbreak on break rule step
- Default selection set to 'winnerbreak' so right arrow is available immediately
- Fallback to stored lastBreakRule or Winnerbreak

Refs #28
2025-10-30 14:04:11 +01:00
Frank Schwenk
3e2264ad9d fix(#28): disable BreakRule right arrow until selection made
- BreakRuleStep now starts with no selection when not prefilled
- Right arrow disabled and dimmed until a rule is chosen (like game type)
- Keeps auto-advance on clicking a rule

Refs #28
2025-10-30 13:57:05 +01:00
Frank Schwenk
77173718c1 fix(#28): add right arrow to BreakRule step and correct back nav
- BreakRule step now shows a right arrow that advances using current selection
- Back navigation from BreakRule returns to Race To instead of exiting to list

Refs #28
2025-10-30 13:47:13 +01:00
Frank Schwenk
ed90b47348 fix(#28): use right arrow on Race To step and fix type issue
- Replace checkmark with right arrow on Race To submit button
- Cast raceTo to string for parseInt in quick-pick highlight

Refs #28
2025-10-30 13:44:20 +01:00
Frank Schwenk
b152575e61 fix(#28): show progress indicator on all steps with 7-step count
- Added indicator to BreakRule and BreakOrder steps
- Updated all steps to display 7 dots and correct active state

Refs #28
2025-10-30 12:32:13 +01:00
Frank Schwenk
f88db204f7 feat(#28): persist last-used break settings in localStorage
- Preload BreakRule and BreakOrder steps from localStorage defaults
- Save selections (rule, first, second) when chosen to speed future setup

Refs #28
2025-10-30 12:27:01 +01:00
Frank Schwenk
bc1bc4b446 fix(#28): finalize game creation on first-break selection
- NewGameScreen now calls onCreateGame after BreakOrder step instead of stopping
- Ensures flow creates game and navigates to detail after selecting breaker

Refs #28
2025-10-30 12:15:38 +01:00
Frank Schwenk
75fc0668bb fix(#28): remove duplicate rendering of BreakRule/BreakOrder steps
- NewGameScreen accidentally rendered break steps twice; cleaned up conditionals

Refs #28
2025-10-30 12:13:59 +01:00
Frank Schwenk
d3083c8c68 chore(#28): use descriptive titles for each creation step
- Player 1/2/3: “Name Spieler X”
- Game type: “Spielart auswählen”
- Race to: “Race To auswählen”
- Break rule: “Break-Regel wählen”
- First break: “Wer bricht zuerst?”

Refs #28
2025-10-30 12:07:17 +01:00
Frank Schwenk
2e0855e781 chore(#28): restore new game step titles without step counters
- Reintroduce 'Neues Spiel' titles on all creation steps
- Remove only the 'Schritt X/Y' portions as requested

Refs #28
2025-10-30 12:03:20 +01:00
Frank Schwenk
81c7c9579b feat(#28): reorder new game steps and remove step labels
- Order: player1 → player2 → player3 → game type → race to → break type → first break
- Removed all step title labels from forms; kept progress dots only
- Adjusted navigation and back behavior accordingly

Refs #28
2025-10-30 12:01:39 +01:00
Frank Schwenk
dc1d9a23a9 feat(#28): add continue arrow and auto-advance in BreakOrder step
- Auto-advance when selecting first breaker for Winnerbreak and 2-player Wechselbreak
- Keep auto-advance after choosing second for 3-player Wechselbreak
- Add explicit right arrow to continue manually based on current selection

Refs #28
2025-10-30 11:51:36 +01:00
Frank Schwenk
a11d41f934 feat(#28): place breaker indicator inline to the right of player name
- Indicator now renders after the name, inline, sized to 1em height
- Tooltip/aria-label remains “Break”

Refs #28
2025-10-30 11:47:46 +01:00
Frank Schwenk
1bd9919b6b feat(#28): add break rule & order to games and UI
- Types: add BreakRule, break metadata to StandardGame, extend NewGameData/steps
- NewGame: add BreakRule and BreakOrder steps with auto-advance
- NewGameScreen: wire new steps into flow
- GameService: set up defaults, persist break order, compute next breaker on +1
- GameDetail: show breaker indicator; treat -1 as undo equivalent

Backfill defaults for existing games via service logic.

Refs #28
2025-10-30 11:34:26 +01:00
Frank Schwenk
147906af59 refactor: remove toast notifications from game detail
- Delete Toast component and styles
- Remove all toast usage from GameDetail

Simplifies UX and eliminates transient notifications.

Refs #26
2025-10-30 11:05:27 +01:00
7 changed files with 256 additions and 230 deletions

View File

@@ -1,7 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import styles from './GameDetail.module.css'; import styles from './GameDetail.module.css';
import Toast from './Toast';
import type { Game, EndlosGame } from '../types/game'; import type { Game, EndlosGame } from '../types/game';
interface GameDetailProps { interface GameDetailProps {
@@ -18,23 +17,11 @@ interface GameDetailProps {
* Game detail view for a single game. * Game detail view for a single game.
*/ */
const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, onForfeit, onBack }: GameDetailProps) => { const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, onForfeit, onBack }: GameDetailProps) => {
const [toast, setToast] = useState<{ show: boolean; message: string; type: 'success' | 'error' | 'info' }>({
show: false,
message: '',
type: 'info'
});
if (!game) return null; if (!game) return null;
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
setToast({ show: true, message, type });
};
const handleScoreUpdate = (playerIndex: number, change: number) => { const handleScoreUpdate = (playerIndex: number, change: number) => {
onUpdateScore(playerIndex, change); onUpdateScore(playerIndex, change);
const playerName = [game.player1, game.player2, game.player3][playerIndex - 1]; // Silent update; toast notifications removed
const action = change > 0 ? 'Punkt hinzugefügt' : 'Punkt abgezogen';
showToast(`${action} für ${playerName}`, 'success');
}; };
@@ -58,7 +45,17 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
className={styles['player-score'] + (name === 'Fränky' ? ' ' + styles['franky'] : '')} className={styles['player-score'] + (name === 'Fränky' ? ' ' + styles['franky'] : '')}
key={name + idx} key={name + idx}
> >
<span className={styles['player-name']}>{name}</span> <span className={styles['player-name']}>
{name}
{(() => {
const order = (game as any).breakOrder as number[] | undefined;
const breakerIdx = (game as any).currentBreakerIdx as number | undefined;
if (order && typeof breakerIdx === 'number' && order[breakerIdx] === idx + 1) {
return <span title="Break" aria-label="Break" style={{ display: 'inline-block', width: '1em', height: '1em', borderRadius: '50%', background: '#fff', marginLeft: 6, verticalAlign: 'middle' }} />;
}
return null;
})()}
</span>
<div className={styles['progress-bar']}> <div className={styles['progress-bar']}>
<div <div
className={styles['progress-fill']} className={styles['progress-fill']}
@@ -85,7 +82,7 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
<button <button
className={styles['score-button']} className={styles['score-button']}
disabled={isCompleted} disabled={isCompleted}
onClick={() => handleScoreUpdate(idx+1, -1)} onClick={() => (onUndo ? onUndo() : undefined)}
aria-label={`Punkt abziehen für ${name}`} aria-label={`Punkt abziehen für ${name}`}
title={`Punkt abziehen für ${name}`} title={`Punkt abziehen für ${name}`}
>-</button> >-</button>
@@ -108,7 +105,6 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
className="btn btn--secondary" className="btn btn--secondary"
onClick={() => { onClick={() => {
onUndo(); onUndo();
showToast('Letzte Aktion rückgängig gemacht', 'info');
}} }}
aria-label="Rückgängig" aria-label="Rückgängig"
> >
@@ -117,12 +113,6 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
)} )}
<button className="btn" disabled={isCompleted} onClick={onFinishGame} aria-label={isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}>{isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}</button> <button className="btn" disabled={isCompleted} onClick={onFinishGame} aria-label={isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}>{isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}</button>
</div> </div>
<Toast
show={toast.show}
message={toast.message}
type={toast.type}
onClose={() => setToast({ show: false, message: '', type: 'info' })}
/>
</div> </div>
); );
}; };

View File

@@ -14,6 +14,7 @@ import {
FORM_CONFIG, FORM_CONFIG,
ERROR_STYLES ERROR_STYLES
} from '../utils/constants'; } from '../utils/constants';
import type { BreakRule } from '../types/game';
interface PlayerSelectModalProps { interface PlayerSelectModalProps {
players: string[]; players: string[];
@@ -126,13 +127,15 @@ const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }:
return ( return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 1 Eingabe" autoComplete="off"> <form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 1 Eingabe" autoComplete="off">
<div className={styles['screen-title']}>Neues Spiel Schritt {WIZARD_STEPS.PLAYER1}/{UI_CONSTANTS.TOTAL_WIZARD_STEPS}</div> <div className={styles['screen-title']}>Name Spieler 1</div>
<div className={styles['progress-indicator']} style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM_MEDIUM }}> <div className={styles['progress-indicator']} style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM_MEDIUM }}>
<span className={styles['progress-dot'] + ' ' + styles['active']} /> <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']} />
<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']} />
<span className={styles['progress-dot']} />
</div> </div>
<div className={styles['player-input'] + ' ' + styles['player1-input']} style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM_LARGE, position: 'relative' }}> <div className={styles['player-input'] + ' ' + styles['player1-input']} style={{ marginBottom: UI_CONSTANTS.MARGIN_BOTTOM_LARGE, position: 'relative' }}>
<label htmlFor="player1-input" style={{ fontSize: UI_CONSTANTS.LABEL_FONT_SIZE, fontWeight: 600 }}>Spieler 1</label> <label htmlFor="player1-input" style={{ fontSize: UI_CONSTANTS.LABEL_FONT_SIZE, fontWeight: 600 }}>Spieler 1</label>
@@ -353,13 +356,15 @@ const Player2Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }:
return ( return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 2 Eingabe" autoComplete="off"> <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['screen-title']}>Name Spieler 2</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}> <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'] + ' ' + styles['active']} />
<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']} /> <span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div> </div>
<div className={styles['player-input'] + ' ' + styles['player2-input']} style={{ marginBottom: 32, position: 'relative' }}> <div className={styles['player-input'] + ' ' + styles['player2-input']} style={{ marginBottom: 32, position: 'relative' }}>
<label htmlFor="player2-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 2</label> <label htmlFor="player2-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 2</label>
@@ -497,13 +502,15 @@ const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }:
return ( return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spieler 3 Eingabe" autoComplete="off"> <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['screen-title']}>Name Spieler 3 (optional)</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}> <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'] + ' ' + styles['active']} />
<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']} />
<span className={styles['progress-dot']} />
</div> </div>
<div className={styles['player-input'] + ' ' + styles['player3-input']} style={{ marginBottom: 32, position: 'relative' }}> <div className={styles['player-input'] + ' ' + styles['player3-input']} style={{ marginBottom: 32, position: 'relative' }}>
<label htmlFor="player3-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 3 (optional)</label> <label htmlFor="player3-input" style={{ fontSize: '1.3rem', fontWeight: 600 }}>Spieler 3 (optional)</label>
@@ -625,13 +632,15 @@ const GameTypeStep = ({ onNext, onCancel, initialValue = '' }: GameTypeStepProps
return ( return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Spielart auswählen"> <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['screen-title']}>Spielart auswählen</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}> <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']} /> <span className={styles['progress-dot']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} /> <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>
<div className={styles['game-type-selection']}> <div className={styles['game-type-selection']}>
{gameTypes.map(type => ( {gameTypes.map(type => (
@@ -729,13 +738,15 @@ const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToSte
return ( return (
<form className={styles['new-game-form']} onSubmit={handleSubmit} aria-label="Race To auswählen"> <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['screen-title']}>Race To auswählen</div>
<div className={styles['progress-indicator']} style={{ marginBottom: 24 }}> <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']} /> <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'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot']} />
</div> </div>
<div className={styles['endlos-container']}> <div className={styles['endlos-container']}>
<button <button
@@ -751,7 +762,7 @@ const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToSte
<button <button
key={value} key={value}
type="button" type="button"
className={`${styles['race-to-btn']} ${parseInt(raceTo, 10) === value ? styles.selected : ''}`} className={`${styles['race-to-btn']} ${parseInt(String(raceTo), 10) === value ? styles.selected : ''}`}
onClick={() => handleQuickPick(value)} onClick={() => handleQuickPick(value)}
> >
{value} {value}
@@ -782,15 +793,143 @@ const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToSte
<button <button
type="submit" type="submit"
className={styles['arrow-btn']} className={styles['arrow-btn']}
aria-label="Fertigstellen" 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' }} 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 */} {/* Unicode right arrow */}
&#10003; &#8594;
</button> </button>
</div> </div>
</form> </form>
); );
}; };
export { Player1Step, Player2Step, Player3Step, GameTypeStep, RaceToStep }; interface BreakRuleStepProps {
onNext: (rule: BreakRule) => void;
onCancel: () => void;
initialValue?: BreakRule | '';
}
const BreakRuleStep = ({ onNext, onCancel, initialValue = 'winnerbreak' }: BreakRuleStepProps) => {
const [rule, setRule] = useState<BreakRule>(initialValue as BreakRule);
return (
<form className={styles['new-game-form']} aria-label="Break-Regel wählen">
<div className={styles['screen-title']}>Break-Regel wä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']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} />
<span className={styles['progress-dot']} />
</div>
<div style={{ display: 'flex', gap: 12, marginTop: 12 }}>
{[
{ key: 'winnerbreak', label: 'Winnerbreak' },
{ key: 'wechselbreak', label: 'Wechselbreak' },
].map(opt => (
<button
key={opt.key}
type="button"
className={`${styles['quick-pick-btn']} ${rule === (opt.key as BreakRule) ? styles['selected'] : ''}`}
onClick={() => { setRule(opt.key as BreakRule); onNext(opt.key as BreakRule); }}
aria-label={`Break-Regel wählen: ${opt.label}`}
>
{opt.label}
</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="button" className={styles['arrow-btn']} aria-label="Weiter" onClick={() => onNext(rule)} 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>
);
};
interface BreakOrderStepProps {
players: string[];
rule: BreakRule;
onNext: (first: number, second?: number) => void;
onCancel: () => void;
initialFirst?: number;
initialSecond?: number;
}
const BreakOrderStep = ({ players, rule, onNext, onCancel, initialFirst = 1, initialSecond }: BreakOrderStepProps) => {
const playerCount = players.filter(Boolean).length;
const [first, setFirst] = useState<number>(initialFirst);
const [second, setSecond] = useState<number>(initialSecond ?? (playerCount >= 2 ? 2 : 1));
const handleFirst = (idx: number) => {
setFirst(idx);
// Auto-advance cases: winnerbreak (any players) OR wechselbreak with 2 players
if (rule === 'winnerbreak' || (rule === 'wechselbreak' && playerCount === 2)) {
onNext(idx);
}
};
const handleSecond = (idx: number) => {
setSecond(idx);
onNext(first, idx);
};
return (
<form className={styles['new-game-form']} aria-label="Break-Reihenfolge wählen">
<div className={styles['screen-title']}>Wer hat den ersten Anstoss?</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']} />
<span className={styles['progress-dot']} />
<span className={styles['progress-dot'] + ' ' + styles['active']} />
</div>
<div style={{ marginBottom: 16, fontWeight: 600 }}>Wer hat den ersten Anstoss?</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{players.filter(Boolean).map((name, idx) => (
<button key={`first-${idx}`} type="button" className={styles['quick-pick-btn']} onClick={() => handleFirst(idx + 1)} aria-label={`Zuerst: ${name}`}>{name}</button>
))}
</div>
{rule === 'wechselbreak' && playerCount === 3 && (
<>
<div style={{ marginTop: 24, marginBottom: 16, fontWeight: 600 }}>Wer bricht als Zweites?</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{players.filter(Boolean).map((name, idx) => (
<button key={`second-${idx}`} type="button" className={styles['quick-pick-btn']} onClick={() => handleSecond(idx + 1)} aria-label={`Zweites Break: ${name}`}>{name}</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="button"
className={styles['arrow-btn']}
aria-label="Weiter"
onClick={() => {
if (rule === 'wechselbreak' && playerCount === 3) {
handleSecond(second);
} else {
onNext(first);
}
}}
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>
);
};
export { Player1Step, Player2Step, Player3Step, GameTypeStep, BreakRuleStep, BreakOrderStep, RaceToStep };

View File

@@ -1,134 +0,0 @@
.toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
min-width: 300px;
max-width: 400px;
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
transform: translateX(100%);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.toast.show {
transform: translateX(0);
opacity: 1;
}
.toast.hide {
transform: translateX(100%);
opacity: 0;
}
.toastContent {
display: flex;
align-items: center;
padding: 16px 20px;
gap: 12px;
}
.toastIcon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
flex-shrink: 0;
}
.toastMessage {
flex: 1;
font-size: 14px;
font-weight: 500;
color: #333;
line-height: 1.4;
}
.toastClose {
background: none;
border: none;
font-size: 18px;
color: #666;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
flex-shrink: 0;
}
.toastClose:hover {
background: #f0f0f0;
color: #333;
}
/* Toast types */
.toast.success {
border-left: 4px solid #4caf50;
}
.toast.success .toastIcon {
background: #4caf50;
color: #fff;
}
.toast.error {
border-left: 4px solid #f44336;
}
.toast.error .toastIcon {
background: #f44336;
color: #fff;
}
.toast.info {
border-left: 4px solid #2196f3;
}
.toast.info .toastIcon {
background: #2196f3;
color: #fff;
}
/* Animation keyframes */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.toast.show {
animation: slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.toast.hide {
animation: slideOutRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

View File

@@ -1,59 +0,0 @@
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import styles from './Toast.module.css';
type ToastType = 'success' | 'error' | 'info';
interface ToastProps {
show: boolean;
message: string;
type?: ToastType;
onClose: () => void;
duration?: number;
}
/**
* Toast notification component for user feedback
*/
const Toast = ({ show, message, type = 'info', onClose, duration = 3000 }: ToastProps) => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (show) {
setIsVisible(true);
const timer = setTimeout(() => {
setIsVisible(false);
setTimeout(onClose, 300); // Wait for animation to complete
}, duration);
return () => clearTimeout(timer);
}
}, [show, duration, onClose]);
if (!show && !isVisible) return null;
return (
<div className={`${styles.toast} ${styles[type]} ${isVisible ? styles.show : styles.hide}`}>
<div className={styles.toastContent}>
<div className={styles.toastIcon}>
{type === 'success' && '✓'}
{type === 'error' && '✕'}
{type === 'info' && ''}
</div>
<span className={styles.toastMessage}>{message}</span>
<button
className={styles.toastClose}
onClick={() => {
setIsVisible(false);
setTimeout(onClose, 300);
}}
aria-label="Schließen"
>
×
</button>
</div>
</div>
);
};
export default Toast;

View File

@@ -1,6 +1,6 @@
import { h } from 'preact'; import { h } from 'preact';
import { Screen } from '../ui/Layout'; import { Screen } from '../ui/Layout';
import { Player1Step, Player2Step, Player3Step, GameTypeStep, RaceToStep } from '../NewGame'; import { Player1Step, Player2Step, Player3Step, GameTypeStep, BreakRuleStep, BreakOrderStep, RaceToStep } from '../NewGame';
import type { NewGameStep, NewGameData } from '../../types/game'; import type { NewGameStep, NewGameData } from '../../types/game';
interface NewGameScreenProps { interface NewGameScreenProps {
@@ -47,9 +47,27 @@ export default function NewGameScreen({
onStepChange('raceTo'); onStepChange('raceTo');
}; };
const handleBreakRuleNext = (rule: 'winnerbreak' | 'wechselbreak') => {
onDataChange({ breakRule: rule });
try { localStorage.setItem('lastBreakRule', rule); } catch {}
onStepChange('breakOrder');
};
const handleBreakOrderNext = (first: number, second?: number) => {
const finalData = { ...data, breakFirst: first, breakSecond: second ?? '' } as any;
onDataChange({ breakFirst: first, breakSecond: second ?? '' });
try {
localStorage.setItem('lastBreakFirst', String(first));
if (second) localStorage.setItem('lastBreakSecond', String(second));
} catch {}
onCreateGame(finalData as any);
};
const handleRaceToNext = (raceTo: string) => { const handleRaceToNext = (raceTo: string) => {
const finalData = { ...data, raceTo }; const finalData = { ...data, raceTo };
onCreateGame(finalData); // After race to, go to break rule selection
onDataChange({ raceTo });
onStepChange('breakRule');
}; };
const handleStepBack = () => { const handleStepBack = () => {
@@ -66,6 +84,12 @@ export default function NewGameScreen({
case 'raceTo': case 'raceTo':
onStepChange('gameType'); onStepChange('gameType');
break; break;
case 'breakRule':
onStepChange('raceTo');
break;
case 'breakOrder':
onStepChange('breakRule');
break;
default: default:
onCancel(); onCancel();
} }
@@ -107,6 +131,8 @@ export default function NewGameScreen({
initialValue={data.gameType} initialValue={data.gameType}
/> />
)} )}
{step === 'raceTo' && ( {step === 'raceTo' && (
<RaceToStep <RaceToStep
@@ -116,6 +142,25 @@ export default function NewGameScreen({
gameType={data.gameType} gameType={data.gameType}
/> />
)} )}
{step === 'breakRule' && (
<BreakRuleStep
onNext={handleBreakRuleNext}
onCancel={handleStepBack}
initialValue={(data.breakRule as any) || (typeof window !== 'undefined' ? (localStorage.getItem('lastBreakRule') as any) : 'winnerbreak') || 'winnerbreak'}
/>
)}
{step === 'breakOrder' && (
<BreakOrderStep
players={[data.player1, data.player2, data.player3]}
rule={(data.breakRule as any) || 'winnerbreak'}
onNext={handleBreakOrderNext}
onCancel={handleStepBack}
initialFirst={(typeof window !== 'undefined' && localStorage.getItem('lastBreakFirst')) ? parseInt(localStorage.getItem('lastBreakFirst')!, 10) : 1}
initialSecond={(typeof window !== 'undefined' && localStorage.getItem('lastBreakSecond')) ? parseInt(localStorage.getItem('lastBreakSecond')!, 10) : undefined}
/>
)}
</Screen> </Screen>
); );
} }

View File

@@ -1,4 +1,4 @@
import type { Game, GameType, StandardGame, EndlosGame, NewGameData } from '../types/game'; import type { Game, GameType, StandardGame, EndlosGame, NewGameData, BreakRule } from '../types/game';
import { IndexedDBService } from './indexedDBService'; import { IndexedDBService } from './indexedDBService';
const LOCAL_STORAGE_KEY = 'bscscore_games'; const LOCAL_STORAGE_KEY = 'bscscore_games';
@@ -163,6 +163,25 @@ export class GameService {
player2: gameData.player2, player2: gameData.player2,
score1: 0, score1: 0,
score2: 0, score2: 0,
breakRule: (gameData.breakRule as BreakRule) || 'winnerbreak',
breakOrder: (() => {
// Determine break order from inputs, fallback defaults
const players: number[] = [1, 2];
if (gameData.player3?.trim()) players.push(3);
const first = (typeof gameData.breakFirst === 'number' ? gameData.breakFirst : 1) as number;
const second = (typeof gameData.breakSecond === 'number' ? gameData.breakSecond : (players.includes(2) && first !== 2 ? 2 : (players.includes(3) ? 3 : 2))) as number;
const order = [first];
if (players.length === 2) {
order.push(first === 1 ? 2 : 1);
} else {
// 3 players: add chosen second, then the remaining third
order.push(second);
const third = [1,2,3].find(p => p !== first && p !== second)!;
order.push(third);
}
return order;
})(),
currentBreakerIdx: 0,
}; };
if (gameData.player3) { if (gameData.player3) {
@@ -185,6 +204,23 @@ export class GameService {
updated.score3 = Math.max(0, updated.score3 + change); updated.score3 = Math.max(0, updated.score3 + change);
} }
// Breaker logic
if (change > 0) {
if ((updated.breakRule || 'winnerbreak') === 'winnerbreak') {
// Winner keeps break: set breaker to this player
const order = updated.breakOrder || [1,2].concat(updated.score3 !== undefined ? [3] : []);
updated.breakOrder = order;
const idx = order.findIndex(p => p === player);
updated.currentBreakerIdx = idx >= 0 ? idx : 0;
} else {
// Wechselbreak: rotate to next
const order = updated.breakOrder || [1,2].concat(updated.score3 !== undefined ? [3] : []);
updated.breakOrder = order;
const curr = typeof updated.currentBreakerIdx === 'number' ? updated.currentBreakerIdx : 0;
updated.currentBreakerIdx = (curr + 1) % order.length;
}
}
updated.updatedAt = new Date().toISOString(); updated.updatedAt = new Date().toISOString();
return updated; return updated;
} }

View File

@@ -33,6 +33,8 @@ export interface BaseGame {
lastModified?: number; lastModified?: number;
} }
export type BreakRule = 'winnerbreak' | 'wechselbreak';
export interface StandardGame extends BaseGame { export interface StandardGame extends BaseGame {
player1: string; player1: string;
player2: string; player2: string;
@@ -40,6 +42,10 @@ export interface StandardGame extends BaseGame {
score1: number; score1: number;
score2: number; score2: number;
score3?: number; score3?: number;
// Break management
breakRule?: BreakRule; // default winnerbreak for backfill
breakOrder?: number[]; // 1-based player indices, e.g. [1,2] or [1,2,3]
currentBreakerIdx?: number; // index into breakOrder
} }
export interface EndlosGame extends BaseGame { export interface EndlosGame extends BaseGame {
@@ -58,8 +64,11 @@ export interface NewGameData {
player3: string; player3: string;
gameType: GameType | ''; gameType: GameType | '';
raceTo: string; raceTo: string;
breakRule?: BreakRule | '';
breakFirst?: number | '';
breakSecond?: number | '';
} }
export type NewGameStep = 'player1' | 'player2' | 'player3' | 'gameType' | 'raceTo' | null; export type NewGameStep = 'player1' | 'player2' | 'player3' | 'gameType' | 'breakRule' | 'breakOrder' | 'raceTo' | null;
export type GameFilter = 'all' | 'active' | 'completed'; export type GameFilter = 'all' | 'active' | 'completed';