feat(storage): migrate to IndexedDB with localStorage fallback and async app flow

- Add IndexedDB service with schema, indexes, and player stats
- Migrate GameService to async IndexedDB and auto-migrate from localStorage
- Update hooks and App handlers to async; add error handling and UX feedback
- Convert remaining JSX components to TSX
- Add test utility for IndexedDB and migration checks
- Extend game types with sync fields for future online sync
This commit is contained in:
Frank Schwenk
2025-10-30 09:36:17 +01:00
parent e89ae1039d
commit 8085d2ecc8
20 changed files with 1288 additions and 277 deletions
+63
View File
@@ -0,0 +1,63 @@
import { h } from 'preact';
import modalStyles from './Modal.module.css';
import styles from './GameCompletionModal.module.css';
import type { Game } from '../types/game';
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;