Files
bscscore/src/components/App.tsx
Frank Schwenk 8085d2ecc8 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
2025-10-30 09:36:17 +01:00

275 lines
9.1 KiB
TypeScript

import { h } from 'preact';
import { useEffect, useCallback } from 'preact/hooks';
import { useGameState } from '../hooks/useGameState';
import { useNavigation, useNewGameWizard } from '../hooks/useNavigation';
import { useModal, useValidationModal, useCompletionModal } from '../hooks/useModal';
import { GameService } from '../services/gameService';
import type { StandardGame, Game, EndlosGame } from '../types/game';
import { Layout } from './ui/Layout';
import GameListScreen from './screens/GameListScreen';
import NewGameScreen from './screens/NewGameScreen';
import GameDetailScreen from './screens/GameDetailScreen';
import Modal from './Modal';
import ValidationModal from './ValidationModal';
import GameCompletionModal from './GameCompletionModal';
import FullscreenToggle from './FullscreenToggle';
/**
* Main App component for BSC Score
*/
export default function App() {
// State management hooks
const gameState = useGameState();
const navigation = useNavigation();
const newGameWizard = useNewGameWizard();
const modal = useModal();
const validationModal = useValidationModal();
const completionModal = useCompletionModal();
// Game lifecycle handlers
const handleCreateGame = useCallback(async (gameData: any) => {
try {
const gameId = await gameState.addGame(gameData);
newGameWizard.resetWizard();
navigation.showGameDetail(gameId);
} catch (error) {
console.error('Failed to create game:', error);
validationModal.showValidation('Failed to create game. Please try again.');
}
}, [gameState.addGame, newGameWizard.resetWizard, navigation.showGameDetail, validationModal.showValidation]);
const handleUpdateScore = useCallback(async (player: number, change: number) => {
if (!navigation.currentGameId) return;
const game = gameState.getGameById(navigation.currentGameId);
if (!game || game.status === 'completed') return;
try {
const updatedGame = GameService.updateGameScore(game as StandardGame, player, change);
// Add undo state for standard games
const gameWithHistory = {
...updatedGame,
undoStack: [...(game.undoStack || []), game],
updatedAt: new Date().toISOString(),
};
await gameState.updateGame(navigation.currentGameId, gameWithHistory);
// Check for completion
if (GameService.isGameCompleted(gameWithHistory)) {
completionModal.openCompletionModal(gameWithHistory);
}
} catch (error) {
console.error('Failed to update score:', error);
validationModal.showValidation('Failed to update score. Please try again.');
}
}, [navigation.currentGameId, gameState.getGameById, gameState.updateGame, completionModal.openCompletionModal, validationModal.showValidation]);
const handleUndo = useCallback(async () => {
if (!navigation.currentGameId) return;
const game = gameState.getGameById(navigation.currentGameId);
if (!game?.undoStack?.length) return;
try {
const lastState = game.undoStack[game.undoStack.length - 1];
const newUndoStack = game.undoStack.slice(0, -1);
await gameState.updateGame(navigation.currentGameId, {
...lastState,
undoStack: newUndoStack,
} as Game);
} catch (error) {
console.error('Failed to undo:', error);
validationModal.showValidation('Failed to undo. Please try again.');
}
}, [navigation.currentGameId, gameState.getGameById, gameState.updateGame, validationModal.showValidation]);
const handleForfeit = async () => {
if (!navigation.currentGameId) return;
const game = gameState.getGameById(navigation.currentGameId);
if (!game || !('players' in game)) return;
try {
const currentPlayerIndex = game.currentPlayer;
if (currentPlayerIndex === null) return;
const winner = game.players.find((_, idx) => idx !== currentPlayerIndex);
const forfeitedGame = {
...game,
status: 'completed' as const,
winner: winner?.name,
forfeitedBy: game.players[currentPlayerIndex].name,
updatedAt: new Date().toISOString(),
};
await gameState.updateGame(navigation.currentGameId, forfeitedGame);
completionModal.openCompletionModal(forfeitedGame);
} catch (error) {
console.error('Failed to forfeit game:', error);
validationModal.showValidation('Failed to forfeit game. Please try again.');
}
};
const handleFinishGame = () => {
if (!navigation.currentGameId) return;
const game = gameState.getGameById(navigation.currentGameId);
if (!game) return;
completionModal.openCompletionModal(game);
};
const handleConfirmCompletion = async () => {
if (!navigation.currentGameId) return;
try {
await gameState.updateGame(navigation.currentGameId, {
...gameState.getGameById(navigation.currentGameId)!,
status: 'completed',
updatedAt: new Date().toISOString(),
});
completionModal.closeCompletionModal();
} catch (error) {
console.error('Failed to complete game:', error);
validationModal.showValidation('Failed to complete game. Please try again.');
}
};
const handleRematch = async () => {
if (!navigation.currentGameId) return;
const completedGame = gameState.getGameById(navigation.currentGameId);
if (!completedGame) return;
let gameData;
if ('players' in completedGame) {
gameData = {
player1: completedGame.players[0]?.name || '',
player2: completedGame.players[1]?.name || '',
player3: completedGame.players[2]?.name || '',
gameType: completedGame.gameType,
raceTo: completedGame.raceTo.toString(),
};
} else {
gameData = {
player1: completedGame.player1,
player2: completedGame.player2,
player3: completedGame.player3 || '',
gameType: completedGame.gameType,
raceTo: completedGame.raceTo.toString(),
};
}
try {
const newGameId = await gameState.addGame(gameData);
completionModal.closeCompletionModal();
navigation.showGameDetail(newGameId);
} catch (error) {
console.error('Failed to create rematch:', error);
validationModal.showValidation('Failed to create rematch. Please try again.');
}
};
const handleDeleteGame = (gameId: number) => {
modal.openModal(gameId);
};
const handleConfirmDelete = async () => {
if (modal.modal.gameId) {
try {
await gameState.deleteGame(modal.modal.gameId);
modal.closeModal();
navigation.showGameList();
} catch (error) {
console.error('Failed to delete game:', error);
validationModal.showValidation('Failed to delete game. Please try again.');
}
}
};
return (
<Layout>
{navigation.screen === 'game-list' && (
<GameListScreen
games={gameState.games}
filter={gameState.filter}
onFilterChange={gameState.setFilter}
onShowGameDetail={navigation.showGameDetail}
onDeleteGame={handleDeleteGame}
onShowNewGame={() => {
newGameWizard.startWizard();
navigation.showNewGame();
}}
/>
)}
{navigation.screen === 'new-game' && (
<NewGameScreen
step={newGameWizard.newGameStep}
data={newGameWizard.newGameData}
playerHistory={gameState.getPlayerNameHistory()}
onStepChange={newGameWizard.nextStep}
onDataChange={newGameWizard.updateGameData}
onCreateGame={handleCreateGame}
onCancel={() => {
newGameWizard.resetWizard();
navigation.showGameList();
}}
onShowValidation={validationModal.showValidation}
/>
)}
{navigation.screen === 'game-detail' && navigation.currentGameId && (
<GameDetailScreen
game={gameState.getGameById(navigation.currentGameId)}
onUpdateScore={handleUpdateScore}
onFinishGame={handleFinishGame}
onUpdateGame={async (game: EndlosGame) => {
try {
await gameState.updateGame(navigation.currentGameId!, game);
} catch (error) {
console.error('Failed to update game:', error);
validationModal.showValidation('Failed to update game. Please try again.');
}
}}
onUndo={handleUndo}
onForfeit={handleForfeit}
onBack={navigation.showGameList}
/>
)}
<Modal
open={modal.modal.open}
title="Spiel löschen"
message="Möchten Sie das Spiel wirklich löschen?"
onCancel={modal.closeModal}
onConfirm={handleConfirmDelete}
/>
<ValidationModal
open={validationModal.validation.open}
message={validationModal.validation.message}
onClose={validationModal.closeValidation}
/>
<GameCompletionModal
open={completionModal.completionModal.open}
game={completionModal.completionModal.game}
onConfirm={handleConfirmCompletion}
onClose={completionModal.closeCompletionModal}
onRematch={handleRematch}
/>
<FullscreenToggle />
</Layout>
);
}