- 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
275 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
} |