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:
@@ -1,12 +1,12 @@
|
||||
import { h } from 'preact';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
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 } from '../types/game';
|
||||
import type { StandardGame, Game, EndlosGame } from '../types/game';
|
||||
|
||||
import { Layout } from './ui/Layout';
|
||||
import GameListScreen from './screens/GameListScreen';
|
||||
@@ -30,72 +30,92 @@ export default function App() {
|
||||
const completionModal = useCompletionModal();
|
||||
|
||||
// Game lifecycle handlers
|
||||
const handleCreateGame = (gameData: any) => {
|
||||
const gameId = gameState.addGame(gameData);
|
||||
newGameWizard.resetWizard();
|
||||
navigation.showGameDetail(gameId);
|
||||
};
|
||||
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 = (player: number, change: number) => {
|
||||
const handleUpdateScore = useCallback(async (player: number, change: number) => {
|
||||
if (!navigation.currentGameId) return;
|
||||
|
||||
const game = gameState.getGameById(navigation.currentGameId);
|
||||
if (!game || game.status === 'completed') return;
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
gameState.updateGame(navigation.currentGameId, gameWithHistory);
|
||||
|
||||
// Check for completion
|
||||
if (GameService.isGameCompleted(gameWithHistory)) {
|
||||
completionModal.openCompletionModal(gameWithHistory);
|
||||
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 = () => {
|
||||
const handleUndo = useCallback(async () => {
|
||||
if (!navigation.currentGameId) return;
|
||||
|
||||
const game = gameState.getGameById(navigation.currentGameId);
|
||||
if (!game?.undoStack?.length) return;
|
||||
|
||||
const lastState = game.undoStack[game.undoStack.length - 1];
|
||||
const newUndoStack = game.undoStack.slice(0, -1);
|
||||
try {
|
||||
const lastState = game.undoStack[game.undoStack.length - 1];
|
||||
const newUndoStack = game.undoStack.slice(0, -1);
|
||||
|
||||
gameState.updateGame(navigation.currentGameId, {
|
||||
...lastState,
|
||||
undoStack: newUndoStack,
|
||||
});
|
||||
};
|
||||
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 = () => {
|
||||
const handleForfeit = async () => {
|
||||
if (!navigation.currentGameId) return;
|
||||
|
||||
const game = gameState.getGameById(navigation.currentGameId);
|
||||
if (!game || !('players' in game)) return;
|
||||
|
||||
const currentPlayerIndex = game.currentPlayer;
|
||||
if (currentPlayerIndex === null) 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(),
|
||||
};
|
||||
|
||||
gameState.updateGame(navigation.currentGameId, forfeitedGame);
|
||||
completionModal.openCompletionModal(forfeitedGame);
|
||||
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 = () => {
|
||||
@@ -107,19 +127,24 @@ export default function App() {
|
||||
completionModal.openCompletionModal(game);
|
||||
};
|
||||
|
||||
const handleConfirmCompletion = () => {
|
||||
const handleConfirmCompletion = async () => {
|
||||
if (!navigation.currentGameId) return;
|
||||
|
||||
gameState.updateGame(navigation.currentGameId, {
|
||||
...gameState.getGameById(navigation.currentGameId)!,
|
||||
status: 'completed',
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
completionModal.closeCompletionModal();
|
||||
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 = () => {
|
||||
const handleRematch = async () => {
|
||||
if (!navigation.currentGameId) return;
|
||||
|
||||
const completedGame = gameState.getGameById(navigation.currentGameId);
|
||||
@@ -144,28 +169,38 @@ export default function App() {
|
||||
};
|
||||
}
|
||||
|
||||
const newGameId = gameState.addGame(gameData);
|
||||
completionModal.closeCompletionModal();
|
||||
navigation.showGameDetail(newGameId);
|
||||
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 = () => {
|
||||
const handleConfirmDelete = async () => {
|
||||
if (modal.modal.gameId) {
|
||||
gameState.deleteGame(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.');
|
||||
}
|
||||
}
|
||||
modal.closeModal();
|
||||
navigation.showGameList();
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
{navigation.screen === 'game-list' && (
|
||||
<GameListScreen
|
||||
games={gameState.getFilteredGames()}
|
||||
games={gameState.games}
|
||||
filter={gameState.filter}
|
||||
onFilterChange={gameState.setFilter}
|
||||
onShowGameDetail={navigation.showGameDetail}
|
||||
@@ -198,7 +233,16 @@ export default function App() {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user