Compare commits
16 Commits
634d012097
...
6da7a5f4e2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da7a5f4e2 | ||
|
|
d22bbdb3dc | ||
|
|
3e2264ad9d | ||
|
|
77173718c1 | ||
|
|
ed90b47348 | ||
|
|
b152575e61 | ||
|
|
f88db204f7 | ||
|
|
bc1bc4b446 | ||
|
|
75fc0668bb | ||
|
|
d3083c8c68 | ||
|
|
2e0855e781 | ||
|
|
81c7c9579b | ||
|
|
dc1d9a23a9 | ||
|
|
a11d41f934 | ||
|
|
1bd9919b6b | ||
|
|
147906af59 |
@@ -1,7 +1,6 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import styles from './GameDetail.module.css';
|
||||
import Toast from './Toast';
|
||||
import type { Game, EndlosGame } from '../types/game';
|
||||
|
||||
interface GameDetailProps {
|
||||
@@ -18,23 +17,11 @@ interface GameDetailProps {
|
||||
* Game detail view for a single game.
|
||||
*/
|
||||
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;
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
setToast({ show: true, message, type });
|
||||
};
|
||||
|
||||
const handleScoreUpdate = (playerIndex: number, change: number) => {
|
||||
onUpdateScore(playerIndex, change);
|
||||
const playerName = [game.player1, game.player2, game.player3][playerIndex - 1];
|
||||
const action = change > 0 ? 'Punkt hinzugefügt' : 'Punkt abgezogen';
|
||||
showToast(`${action} für ${playerName}`, 'success');
|
||||
// Silent update; toast notifications removed
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +45,17 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
|
||||
className={styles['player-score'] + (name === 'Fränky' ? ' ' + styles['franky'] : '')}
|
||||
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-fill']}
|
||||
@@ -85,7 +82,7 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
|
||||
<button
|
||||
className={styles['score-button']}
|
||||
disabled={isCompleted}
|
||||
onClick={() => handleScoreUpdate(idx+1, -1)}
|
||||
onClick={() => (onUndo ? onUndo() : undefined)}
|
||||
aria-label={`Punkt abziehen für ${name}`}
|
||||
title={`Punkt abziehen für ${name}`}
|
||||
>-</button>
|
||||
@@ -108,7 +105,6 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
|
||||
className="btn btn--secondary"
|
||||
onClick={() => {
|
||||
onUndo();
|
||||
showToast('Letzte Aktion rückgängig gemacht', 'info');
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
<Toast
|
||||
show={toast.show}
|
||||
message={toast.message}
|
||||
type={toast.type}
|
||||
onClose={() => setToast({ show: false, message: '', type: 'info' })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
FORM_CONFIG,
|
||||
ERROR_STYLES
|
||||
} from '../utils/constants';
|
||||
import type { BreakRule } from '../types/game';
|
||||
|
||||
interface PlayerSelectModalProps {
|
||||
players: string[];
|
||||
@@ -126,13 +127,15 @@ const Player1Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }:
|
||||
|
||||
return (
|
||||
<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 }}>
|
||||
<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 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>
|
||||
@@ -353,13 +356,15 @@ const Player2Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }:
|
||||
|
||||
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['screen-title']}>Name Spieler 2</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']} />
|
||||
<span className={styles['progress-dot']} />
|
||||
<span className={styles['progress-dot']} />
|
||||
</div>
|
||||
<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>
|
||||
@@ -497,13 +502,15 @@ const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' }:
|
||||
|
||||
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['screen-title']}>Name Spieler 3 (optional)</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']} />
|
||||
<span className={styles['progress-dot']} />
|
||||
<span className={styles['progress-dot']} />
|
||||
</div>
|
||||
<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>
|
||||
@@ -625,13 +632,15 @@ const GameTypeStep = ({ onNext, onCancel, initialValue = '' }: GameTypeStepProps
|
||||
|
||||
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['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 => (
|
||||
@@ -729,13 +738,15 @@ const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToSte
|
||||
|
||||
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['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
|
||||
@@ -751,7 +762,7 @@ const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToSte
|
||||
<button
|
||||
key={value}
|
||||
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)}
|
||||
>
|
||||
{value}
|
||||
@@ -782,15 +793,143 @@ const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }: RaceToSte
|
||||
<button
|
||||
type="submit"
|
||||
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' }}
|
||||
>
|
||||
{/* Unicode checkmark */}
|
||||
✓
|
||||
{/* Unicode right arrow */}
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</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' }}>
|
||||
←
|
||||
</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' }}>
|
||||
→
|
||||
</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' }}>
|
||||
←
|
||||
</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' }}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export { Player1Step, Player2Step, Player3Step, GameTypeStep, BreakRuleStep, BreakOrderStep, RaceToStep };
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { h } from 'preact';
|
||||
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';
|
||||
|
||||
interface NewGameScreenProps {
|
||||
@@ -47,9 +47,27 @@ export default function NewGameScreen({
|
||||
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 finalData = { ...data, raceTo };
|
||||
onCreateGame(finalData);
|
||||
// After race to, go to break rule selection
|
||||
onDataChange({ raceTo });
|
||||
onStepChange('breakRule');
|
||||
};
|
||||
|
||||
const handleStepBack = () => {
|
||||
@@ -66,6 +84,12 @@ export default function NewGameScreen({
|
||||
case 'raceTo':
|
||||
onStepChange('gameType');
|
||||
break;
|
||||
case 'breakRule':
|
||||
onStepChange('raceTo');
|
||||
break;
|
||||
case 'breakOrder':
|
||||
onStepChange('breakRule');
|
||||
break;
|
||||
default:
|
||||
onCancel();
|
||||
}
|
||||
@@ -107,6 +131,8 @@ export default function NewGameScreen({
|
||||
initialValue={data.gameType}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{step === 'raceTo' && (
|
||||
<RaceToStep
|
||||
@@ -116,6 +142,25 @@ export default function NewGameScreen({
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'bscscore_games';
|
||||
@@ -163,6 +163,25 @@ export class GameService {
|
||||
player2: gameData.player2,
|
||||
score1: 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) {
|
||||
@@ -185,6 +204,23 @@ export class GameService {
|
||||
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();
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface BaseGame {
|
||||
lastModified?: number;
|
||||
}
|
||||
|
||||
export type BreakRule = 'winnerbreak' | 'wechselbreak';
|
||||
|
||||
export interface StandardGame extends BaseGame {
|
||||
player1: string;
|
||||
player2: string;
|
||||
@@ -40,6 +42,10 @@ export interface StandardGame extends BaseGame {
|
||||
score1: number;
|
||||
score2: 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 {
|
||||
@@ -58,8 +64,11 @@ export interface NewGameData {
|
||||
player3: string;
|
||||
gameType: GameType | '';
|
||||
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';
|
||||
Reference in New Issue
Block a user