feat: tablet-first UX overhaul with wizard improvements
- Tablet-first responsive scoreboard: whole player card is tap target - Per-step wizard backgrounds and live summary panel - Larger touch targets, safe-area handling, reduced-motion support - German UI error strings in App.tsx - Updated SOUL.md and README.md for current architecture - Cleanup: removed legacy components, .cursor/, UI_IMPROVEMENTS.md - Renamed package to bscscore, fixed TOTAL_WIZARD_STEPS
This commit is contained in:
+38
-21
@@ -11,6 +11,7 @@ import {
|
||||
} from '@lib/state';
|
||||
import { GameService } from '@lib/data/gameService';
|
||||
import type { StandardGame, Game, EndlosGame } from '@lib/domain/types';
|
||||
import { isEndlosGame } from '@lib/domain/gameUtils';
|
||||
|
||||
import { Layout } from '@lib/ui/Layout';
|
||||
import GameListScreen from './screens/GameListScreen';
|
||||
@@ -33,30 +34,46 @@ export default function App() {
|
||||
const validationModal = useValidationModal();
|
||||
const completionModal = useCompletionModal();
|
||||
|
||||
// Keep viewport height stable on Android tablets when virtual keyboard opens.
|
||||
// Keep layout height stable when the virtual keyboard opens on cheap Android tablets.
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
let rafId = 0;
|
||||
let orientationTimer1 = 0;
|
||||
let orientationTimer2 = 0;
|
||||
|
||||
const updateViewportVars = () => {
|
||||
const viewport = window.visualViewport;
|
||||
const appHeight = viewport ? viewport.height : window.innerHeight;
|
||||
const keyboardOffset = viewport
|
||||
? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop)
|
||||
: 0;
|
||||
cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(() => {
|
||||
const viewport = window.visualViewport;
|
||||
const layoutHeight = window.innerHeight;
|
||||
const appHeight = viewport?.height ?? layoutHeight;
|
||||
const keyboardOffset = viewport
|
||||
? Math.max(0, layoutHeight - viewport.height - viewport.offsetTop)
|
||||
: 0;
|
||||
|
||||
root.style.setProperty('--app-height', `${Math.round(appHeight)}px`);
|
||||
root.style.setProperty('--keyboard-offset', `${Math.round(keyboardOffset)}px`);
|
||||
root.style.setProperty('--app-height', `${Math.round(appHeight)}px`);
|
||||
root.style.setProperty('--keyboard-offset', `${Math.round(keyboardOffset)}px`);
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrientationChange = () => {
|
||||
updateViewportVars();
|
||||
orientationTimer1 = window.setTimeout(updateViewportVars, 100);
|
||||
orientationTimer2 = window.setTimeout(updateViewportVars, 350);
|
||||
};
|
||||
|
||||
updateViewportVars();
|
||||
window.addEventListener('resize', updateViewportVars);
|
||||
window.addEventListener('orientationchange', updateViewportVars);
|
||||
window.addEventListener('orientationchange', handleOrientationChange);
|
||||
window.visualViewport?.addEventListener('resize', updateViewportVars);
|
||||
window.visualViewport?.addEventListener('scroll', updateViewportVars);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
window.clearTimeout(orientationTimer1);
|
||||
window.clearTimeout(orientationTimer2);
|
||||
window.removeEventListener('resize', updateViewportVars);
|
||||
window.removeEventListener('orientationchange', updateViewportVars);
|
||||
window.removeEventListener('orientationchange', handleOrientationChange);
|
||||
window.visualViewport?.removeEventListener('resize', updateViewportVars);
|
||||
window.visualViewport?.removeEventListener('scroll', updateViewportVars);
|
||||
};
|
||||
@@ -70,7 +87,7 @@ export default function App() {
|
||||
navigation.showGameDetail(gameId);
|
||||
} catch (error) {
|
||||
console.error('Failed to create game:', error);
|
||||
validationModal.showValidation('Failed to create game. Please try again.');
|
||||
validationModal.showValidation('Spiel konnte nicht erstellt werden.');
|
||||
}
|
||||
}, [gameState.addGame, newGameWizard.resetWizard, navigation.showGameDetail, validationModal.showValidation]);
|
||||
|
||||
@@ -99,7 +116,7 @@ export default function App() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update score:', error);
|
||||
validationModal.showValidation('Failed to update score. Please try again.');
|
||||
validationModal.showValidation('Punkt konnte nicht gespeichert werden.');
|
||||
}
|
||||
}, [navigation.currentGameId, gameState.getGameById, gameState.updateGame, completionModal.openCompletionModal, validationModal.showValidation]);
|
||||
|
||||
@@ -120,7 +137,7 @@ export default function App() {
|
||||
} as Game);
|
||||
} catch (error) {
|
||||
console.error('Failed to undo:', error);
|
||||
validationModal.showValidation('Failed to undo. Please try again.');
|
||||
validationModal.showValidation('Rückgängig nicht möglich.');
|
||||
}
|
||||
}, [navigation.currentGameId, gameState.getGameById, gameState.updateGame, validationModal.showValidation]);
|
||||
|
||||
@@ -128,7 +145,7 @@ export default function App() {
|
||||
if (!navigation.currentGameId) return;
|
||||
|
||||
const game = gameState.getGameById(navigation.currentGameId);
|
||||
if (!game || !('players' in game)) return;
|
||||
if (!game || !isEndlosGame(game)) return;
|
||||
|
||||
try {
|
||||
const currentPlayerIndex = game.currentPlayer;
|
||||
@@ -147,7 +164,7 @@ export default function App() {
|
||||
completionModal.openCompletionModal(forfeitedGame);
|
||||
} catch (error) {
|
||||
console.error('Failed to forfeit game:', error);
|
||||
validationModal.showValidation('Failed to forfeit game. Please try again.');
|
||||
validationModal.showValidation('Aufgeben nicht möglich.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,7 +190,7 @@ export default function App() {
|
||||
completionModal.closeCompletionModal();
|
||||
} catch (error) {
|
||||
console.error('Failed to complete game:', error);
|
||||
validationModal.showValidation('Failed to complete game. Please try again.');
|
||||
validationModal.showValidation('Spiel konnte nicht abgeschlossen werden.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,7 +201,7 @@ export default function App() {
|
||||
if (!completedGame) return;
|
||||
|
||||
let gameData;
|
||||
if ('players' in completedGame) {
|
||||
if (isEndlosGame(completedGame)) {
|
||||
gameData = {
|
||||
player1: completedGame.players[0]?.name || '',
|
||||
player2: completedGame.players[1]?.name || '',
|
||||
@@ -208,7 +225,7 @@ export default function App() {
|
||||
navigation.showGameDetail(newGameId);
|
||||
} catch (error) {
|
||||
console.error('Failed to create rematch:', error);
|
||||
validationModal.showValidation('Failed to create rematch. Please try again.');
|
||||
validationModal.showValidation('Rematch konnte nicht erstellt werden.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -224,7 +241,7 @@ export default function App() {
|
||||
navigation.showGameList();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete game:', error);
|
||||
validationModal.showValidation('Failed to delete game. Please try again.');
|
||||
validationModal.showValidation('Spiel konnte nicht gelöscht werden.');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -271,7 +288,7 @@ export default function App() {
|
||||
await gameState.updateGame(navigation.currentGameId!, game);
|
||||
} catch (error) {
|
||||
console.error('Failed to update game:', error);
|
||||
validationModal.showValidation('Failed to update game. Please try again.');
|
||||
validationModal.showValidation('Spiel konnte nicht aktualisiert werden.');
|
||||
}
|
||||
}}
|
||||
onUndo={handleUndo}
|
||||
@@ -305,4 +322,4 @@ export default function App() {
|
||||
<FullscreenToggle />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
// This is an Astro component that properly leverages SSR and islands
|
||||
---
|
||||
|
||||
<!-- Use Astro's islands architecture for better performance -->
|
||||
<!-- Only hydrate the interactive app component when needed -->
|
||||
<div id="app-root">
|
||||
<slot name="app-content" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#app-root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Progressive enhancement styles */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Client-side progressive enhancement
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Add any progressive enhancement here
|
||||
console.log('BSC Score App initialized');
|
||||
});
|
||||
</script>
|
||||
@@ -1,38 +1,47 @@
|
||||
/* FullscreenToggle-specific styles only. */
|
||||
/* FullscreenToggle — top-right keeps bottom controls clear on scoreboard */
|
||||
.fullscreenToggle {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
top: calc(var(--space-md) + var(--safe-area-top));
|
||||
right: calc(var(--space-md) + var(--safe-area-right));
|
||||
width: var(--touch-target-comfortable);
|
||||
height: var(--touch-target-comfortable);
|
||||
border-radius: 50%;
|
||||
background-color: rgba(52, 152, 219, 0.9);
|
||||
border: none;
|
||||
color: white;
|
||||
background-color: var(--color-secondary);
|
||||
border: 2px solid var(--color-border);
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 9999;
|
||||
transition: background-color 0.2s, transform 0.2s;
|
||||
transition: background-color var(--transition-base), opacity var(--transition-base);
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.fullscreenToggle:hover {
|
||||
background-color: rgba(52, 152, 219, 1);
|
||||
transform: scale(1.1);
|
||||
|
||||
.fullscreenToggle:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.fullscreenToggle:active {
|
||||
transform: scale(0.95);
|
||||
opacity: 0.8;
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.fullscreenToggle svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.fullscreenToggle {
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: var(--touch-target-large);
|
||||
height: var(--touch-target-large);
|
||||
}
|
||||
}
|
||||
|
||||
.fullscreenToggle svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const FullscreenToggle = () => {
|
||||
<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
|
||||
<path fill="currentColor" d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||
</svg>
|
||||
<span className="sr-only">Vollbild</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import modalStyles from '@lib/ui/Modal.module.css';
|
||||
import styles from './GameCompletionModal.module.css';
|
||||
import type { Game } from '@lib/domain/types';
|
||||
|
||||
interface GameCompletionModalProps {
|
||||
open: boolean;
|
||||
game: Game | null;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
onRematch: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal shown when a game is completed.
|
||||
*/
|
||||
const GameCompletionModal = ({ open, game, onConfirm, onClose, onRematch }: GameCompletionModalProps) => {
|
||||
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]);
|
||||
let maxScore, winners, winnerText;
|
||||
|
||||
if (game.forfeitedBy) {
|
||||
winnerText = `${game.winner} hat gewonnen, da ${game.forfeitedBy} aufgegeben hat.`;
|
||||
} else {
|
||||
maxScore = Math.max(...scores);
|
||||
winners = playerNames.filter((name, idx) => scores[idx] === maxScore);
|
||||
winnerText = winners.length > 1
|
||||
? `Unentschieden zwischen ${winners.join(' und ')}`
|
||||
: `${winners[0]} hat gewonnen!`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="game-completion-modal" className={modalStyles['modal'] + ' ' + modalStyles['show']} role="dialog" aria-modal="true" aria-labelledby="completion-modal-title">
|
||||
<div className={modalStyles['modal-content']}>
|
||||
<div className={modalStyles['modal-header']}>
|
||||
<span className={modalStyles['modal-title']} id="completion-modal-title">Spiel beendet</span>
|
||||
<button className={modalStyles['close-button']} onClick={onClose} aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div className={modalStyles['modal-body']}>
|
||||
<div className={styles['final-scores']}>
|
||||
{playerNames.map((name, idx) => (
|
||||
<div className={styles['final-score']} key={name + idx}>
|
||||
<span className={styles['player-name']}>{name}</span>
|
||||
<span className={styles['score']}>{scores[idx]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles['winner-announcement']}><h3>{winnerText}</h3></div>
|
||||
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameCompletionModal;
|
||||
@@ -1,105 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import styles from './GameDetail.module.css';
|
||||
import type { Game, EndlosGame } from '@lib/domain/types';
|
||||
|
||||
interface GameDetailProps {
|
||||
game: Game | undefined;
|
||||
onFinishGame: () => void;
|
||||
onUpdateScore: (player: number, change: number) => void;
|
||||
onUpdateGame?: (game: EndlosGame) => void;
|
||||
onUndo?: () => void;
|
||||
onForfeit?: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Game detail view for a single game.
|
||||
*/
|
||||
const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, onForfeit, onBack }: GameDetailProps) => {
|
||||
if (!game) return null;
|
||||
|
||||
const handleScoreUpdate = (playerIndex: number, change: number) => {
|
||||
onUpdateScore(playerIndex, change);
|
||||
// Silent update; toast notifications removed
|
||||
};
|
||||
|
||||
|
||||
const isCompleted = game.status === 'completed';
|
||||
|
||||
const playerNames = [game.player1, game.player2, game.player3].filter(Boolean);
|
||||
const scores = [game.score1, game.score2, game.score3].filter((_, i) => playerNames[i]);
|
||||
|
||||
return (
|
||||
<div className={styles['game-detail']}>
|
||||
<div className={styles['game-title']}>
|
||||
{game.gameType}{game.raceTo ? ` | Race to ${game.raceTo}` : ''}
|
||||
</div>
|
||||
<div className={styles['scores-container']}>
|
||||
{playerNames.map((name, idx) => {
|
||||
const currentScore = scores[idx];
|
||||
const progressPercentage = game.raceTo ? Math.min((currentScore / game.raceTo) * 100, 100) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles['player-score'] + (name === 'Fränky' ? ' ' + styles['franky'] : '')}
|
||||
key={name + idx}
|
||||
>
|
||||
<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']}
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={styles['score']}
|
||||
id={`score${idx + 1}`}
|
||||
onClick={() => !isCompleted && onUpdateScore(idx + 1, 1)}
|
||||
onKeyDown={(e) => {
|
||||
if (!isCompleted && (e.key === 'Enter' || e.key === ' ')) {
|
||||
e.preventDefault();
|
||||
onUpdateScore(idx + 1, 1);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={isCompleted ? -1 : 0}
|
||||
aria-label={`Aktueller Punktestand für ${name}: ${scores[idx]}. Klicken oder Enter drücken zum Erhöhen.`}
|
||||
>
|
||||
{scores[idx]}
|
||||
</span>
|
||||
{/* +/- buttons removed per issue #29. Tap score to +1; use Undo to revert. */}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles['game-detail-controls']}>
|
||||
<button className="btn" onClick={onBack} aria-label="Zurück zur Liste">Zurück zur Liste</button>
|
||||
{onUndo && (
|
||||
<button
|
||||
className="btn btn--secondary"
|
||||
onClick={() => {
|
||||
onUndo();
|
||||
}}
|
||||
aria-label="Rückgängig"
|
||||
>
|
||||
↶ Rückgängig
|
||||
</button>
|
||||
)}
|
||||
<button className="btn" disabled={isCompleted} onClick={onFinishGame} aria-label={isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}>{isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameDetail;
|
||||
@@ -1,129 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import { Card } from '@lib/ui/Card';
|
||||
import { Button } from '@lib/ui/Button';
|
||||
import styles from './GameList.module.css';
|
||||
import type { Game, GameFilter, StandardGame } from '@lib/domain/types';
|
||||
|
||||
interface GameListProps {
|
||||
games: Game[];
|
||||
filter: GameFilter;
|
||||
setFilter: (filter: GameFilter) => void;
|
||||
onShowGameDetail: (gameId: number) => void;
|
||||
onDeleteGame: (gameId: number) => void;
|
||||
}
|
||||
|
||||
export default function GameList({
|
||||
games,
|
||||
filter = 'all',
|
||||
setFilter,
|
||||
onShowGameDetail,
|
||||
onDeleteGame
|
||||
}: GameListProps) {
|
||||
const filteredGames = games
|
||||
.filter(game => {
|
||||
if (filter === 'active') return game.status === 'active';
|
||||
if (filter === 'completed') return game.status === 'completed';
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
const getPlayerNames = (game: Game): string => {
|
||||
if ('players' in game) {
|
||||
return game.players.map(p => p.name).join(' vs ');
|
||||
} else {
|
||||
const standardGame = game as StandardGame;
|
||||
return standardGame.player3
|
||||
? `${standardGame.player1} vs ${standardGame.player2} vs ${standardGame.player3}`
|
||||
: `${standardGame.player1} vs ${standardGame.player2}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getScores = (game: Game): string => {
|
||||
if ('players' in game) {
|
||||
return game.players.map(p => p.score).join(' - ');
|
||||
} else {
|
||||
const standardGame = game as StandardGame;
|
||||
return standardGame.player3
|
||||
? `${standardGame.score1} - ${standardGame.score2} - ${standardGame.score3}`
|
||||
: `${standardGame.score1} - ${standardGame.score2}`;
|
||||
}
|
||||
};
|
||||
|
||||
const filterButtons = [
|
||||
{ key: 'all' as const, label: 'Alle', ariaLabel: 'Alle Spiele anzeigen' },
|
||||
{ key: 'active' as const, label: 'Aktiv', ariaLabel: 'Nur aktive Spiele anzeigen' },
|
||||
{ key: 'completed' as const, label: 'Abgeschlossen', ariaLabel: 'Nur abgeschlossene Spiele anzeigen' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles['game-list']}>
|
||||
<div className={styles['filter-buttons']}>
|
||||
{filterButtons.map(({ key, label, ariaLabel }) => (
|
||||
<Button
|
||||
key={key}
|
||||
variant={filter === key ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
onClick={() => setFilter(key)}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles['games-container']}>
|
||||
{filteredGames.length === 0 ? (
|
||||
<div className={styles['empty-state']}>Keine Spiele vorhanden</div>
|
||||
) : (
|
||||
filteredGames.map(game => {
|
||||
const playerNames = getPlayerNames(game);
|
||||
const scores = getScores(game);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={game.id}
|
||||
variant="elevated"
|
||||
className={
|
||||
styles['game-item'] + ' ' + (game.status === 'completed' ? styles['completed'] : styles['active'])
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={styles['game-info']}
|
||||
onClick={() => onShowGameDetail(game.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onShowGameDetail(game.id);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Details für Spiel ${playerNames}`}
|
||||
aria-describedby={`game-${game.id}-description`}
|
||||
>
|
||||
<div className={styles['game-type']}>
|
||||
{game.gameType}{game.raceTo ? ` | ${game.raceTo}` : ''}
|
||||
</div>
|
||||
<div className={styles['player-names']}>{playerNames}</div>
|
||||
<div className={styles['game-scores']}>{scores}</div>
|
||||
<div id={`game-${game.id}-description`} className="sr-only">
|
||||
{game.gameType} Spiel zwischen {playerNames} mit dem Stand {scores}.
|
||||
{game.status === 'completed' ? 'Abgeschlossen' : 'Aktiv'}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="small"
|
||||
onClick={() => onDeleteGame(game.id)}
|
||||
aria-label={`Spiel löschen: ${playerNames}`}
|
||||
>
|
||||
🗑
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export default function GameDetailScreen({
|
||||
if (!game) {
|
||||
return (
|
||||
<Screen>
|
||||
<div>Game not found</div>
|
||||
<div>Spiel nicht gefunden</div>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@lib/ui/Button';
|
||||
import { Screen } from '@lib/ui/Layout';
|
||||
import GameList from '@lib/features/game-list/GameList';
|
||||
import type { Game, GameFilter } from '@lib/domain/types';
|
||||
import styles from '@lib/features/game-list/GameList.module.css';
|
||||
|
||||
interface GameListScreenProps {
|
||||
games: Game[];
|
||||
@@ -23,13 +24,12 @@ export default function GameListScreen({
|
||||
}: GameListScreenProps) {
|
||||
return (
|
||||
<Screen>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div className={styles.newGameWrapper}>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
onClick={onShowNewGame}
|
||||
aria-label="Neues Spiel starten"
|
||||
style={{ width: '100%', marginBottom: '24px' }}
|
||||
>
|
||||
+ Neues Spiel
|
||||
</Button>
|
||||
@@ -44,4 +44,4 @@ export default function GameListScreen({
|
||||
/>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ export default function NewGameScreen({
|
||||
const playerStepConfig: PlayerStepConfigMap = {
|
||||
player1: {
|
||||
stepNumber: 1,
|
||||
stepClassName: newGameStyles['step-player1'],
|
||||
title: 'Name Spieler 1',
|
||||
label: 'Spieler 1',
|
||||
placeholder: 'Name Spieler 1',
|
||||
@@ -145,6 +146,7 @@ export default function NewGameScreen({
|
||||
},
|
||||
player2: {
|
||||
stepNumber: 2,
|
||||
stepClassName: newGameStyles['step-player2'],
|
||||
title: 'Name Spieler 2',
|
||||
label: 'Spieler 2',
|
||||
placeholder: 'Name Spieler 2',
|
||||
@@ -165,6 +167,7 @@ export default function NewGameScreen({
|
||||
},
|
||||
player3: {
|
||||
stepNumber: 3,
|
||||
stepClassName: newGameStyles['step-player3'],
|
||||
title: 'Name Spieler 3 (optional)',
|
||||
label: 'Spieler 3 (optional)',
|
||||
placeholder: 'Name Spieler 3 (optional)',
|
||||
@@ -193,6 +196,8 @@ export default function NewGameScreen({
|
||||
key={step}
|
||||
{...playerStepConfig[step]}
|
||||
playerNameHistory={playerNameOptions}
|
||||
data={data}
|
||||
currentStep={step}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -201,6 +206,7 @@ export default function NewGameScreen({
|
||||
onNext={handleGameTypeNext}
|
||||
onCancel={handleStepBack}
|
||||
initialValue={data.gameType}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -212,6 +218,7 @@ export default function NewGameScreen({
|
||||
onCancel={handleStepBack}
|
||||
initialValue={data.raceTo}
|
||||
gameType={data.gameType}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -220,6 +227,7 @@ export default function NewGameScreen({
|
||||
onNext={handleBreakRuleNext}
|
||||
onCancel={handleStepBack}
|
||||
initialValue={(data.breakRule as any) || (typeof window !== 'undefined' ? (localStorage.getItem('lastBreakRule') as any) : 'winnerbreak') || 'winnerbreak'}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -231,6 +239,7 @@ export default function NewGameScreen({
|
||||
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}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
</Screen>
|
||||
|
||||
Reference in New Issue
Block a user