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:
@@ -45,4 +45,89 @@ export const BREAKPOINTS = {
|
||||
TABLET: '768px',
|
||||
DESKTOP: '1024px',
|
||||
LARGE_DESKTOP: '1200px',
|
||||
} as const;
|
||||
|
||||
export const UI_CONSTANTS = {
|
||||
// Progress indicators
|
||||
TOTAL_WIZARD_STEPS: 5,
|
||||
|
||||
// Input styling
|
||||
INPUT_FONT_SIZE: '1.2rem',
|
||||
LABEL_FONT_SIZE: '1.3rem',
|
||||
INPUT_MIN_HEIGHT: 48,
|
||||
INPUT_PADDING_RIGHT: 44,
|
||||
|
||||
// Button styling
|
||||
ARROW_BUTTON_SIZE: 80,
|
||||
ARROW_BUTTON_FONT_SIZE: 48,
|
||||
QUICK_PICK_PADDING: '12px 20px',
|
||||
QUICK_PICK_FONT_SIZE: '1.1rem',
|
||||
|
||||
// Spacing
|
||||
MARGIN_BOTTOM_LARGE: 32,
|
||||
MARGIN_BOTTOM_MEDIUM: 24,
|
||||
MARGIN_BOTTOM_SMALL: 16,
|
||||
MARGIN_TOP_NAV: 48,
|
||||
|
||||
// Quick pick limits
|
||||
MAX_QUICK_PICKS: 10,
|
||||
|
||||
// Animation durations
|
||||
TOAST_DURATION: 3000,
|
||||
TOAST_ANIMATION_DELAY: 300,
|
||||
} as const;
|
||||
|
||||
export const WIZARD_STEPS = {
|
||||
PLAYER1: 1,
|
||||
PLAYER2: 2,
|
||||
PLAYER3: 3,
|
||||
GAME_TYPE: 4,
|
||||
RACE_TO: 5,
|
||||
} as const;
|
||||
|
||||
export const GAME_TYPE_OPTIONS = ['8-Ball', '9-Ball', '10-Ball'] as const;
|
||||
|
||||
export const RACE_TO_QUICK_PICKS = [1, 2, 3, 4, 5, 6, 7, 8, 9] as const;
|
||||
|
||||
export const RACE_TO_DEFAULT = 5;
|
||||
|
||||
export const RACE_TO_INFINITY = 'Infinity';
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
PLAYER1_REQUIRED: 'Bitte Namen für Spieler 1 eingeben',
|
||||
PLAYER2_REQUIRED: 'Bitte Namen für Spieler 2 eingeben',
|
||||
GAME_TYPE_REQUIRED: 'Spieltyp muss ausgewählt werden',
|
||||
} as const;
|
||||
|
||||
export const ARIA_LABELS = {
|
||||
BACK: 'Zurück',
|
||||
NEXT: 'Weiter',
|
||||
SKIP: 'Überspringen',
|
||||
CLEAR_FIELD: 'Feld leeren',
|
||||
SHOW_MORE_PLAYERS: 'Weitere Spieler anzeigen',
|
||||
QUICK_PICK: (name: string) => `Schnellauswahl: ${name}`,
|
||||
PLAYER_INPUT: (step: string) => `${step} Eingabe`,
|
||||
} as const;
|
||||
|
||||
export const FORM_CONFIG = {
|
||||
MAX_PLAYER_NAME_LENGTH: 20,
|
||||
CHARACTER_COUNT_WARNING_THRESHOLD: 15,
|
||||
VALIDATION_DEBOUNCE_MS: 300,
|
||||
} as const;
|
||||
|
||||
export const ERROR_STYLES = {
|
||||
CONTAINER: {
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#ffebee',
|
||||
border: '1px solid #f44336',
|
||||
borderRadius: '4px',
|
||||
color: '#d32f2f',
|
||||
fontSize: '0.875rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
},
|
||||
ICON: {
|
||||
fontSize: '16px',
|
||||
},
|
||||
} as const;
|
||||
140
src/utils/testIndexedDB.ts
Normal file
140
src/utils/testIndexedDB.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { IndexedDBService } from '../services/indexedDBService';
|
||||
import { GameService } from '../services/gameService';
|
||||
import type { NewGameData } from '../types/game';
|
||||
|
||||
/**
|
||||
* Test utility for IndexedDB functionality
|
||||
* Run this in the browser console to test the implementation
|
||||
*/
|
||||
export async function testIndexedDB() {
|
||||
console.log('🧪 Starting IndexedDB tests...');
|
||||
|
||||
try {
|
||||
// Test 1: Initialize IndexedDB
|
||||
console.log('Test 1: Initializing IndexedDB...');
|
||||
await IndexedDBService.init();
|
||||
console.log('✅ IndexedDB initialized successfully');
|
||||
|
||||
// Test 2: Create a test game
|
||||
console.log('Test 2: Creating test game...');
|
||||
const testGameData: NewGameData = {
|
||||
player1: 'Test Player 1',
|
||||
player2: 'Test Player 2',
|
||||
player3: 'Test Player 3',
|
||||
gameType: '8-Ball',
|
||||
raceTo: '5'
|
||||
};
|
||||
|
||||
const testGame = GameService.createGame(testGameData);
|
||||
console.log('✅ Test game created:', testGame);
|
||||
|
||||
// Test 3: Save game to IndexedDB
|
||||
console.log('Test 3: Saving game to IndexedDB...');
|
||||
await IndexedDBService.saveGame(testGame);
|
||||
console.log('✅ Game saved to IndexedDB');
|
||||
|
||||
// Test 4: Load games from IndexedDB
|
||||
console.log('Test 4: Loading games from IndexedDB...');
|
||||
const loadedGames = await IndexedDBService.loadGames();
|
||||
console.log('✅ Games loaded:', loadedGames.length, 'games found');
|
||||
|
||||
// Test 5: Test filtering
|
||||
console.log('Test 5: Testing game filtering...');
|
||||
const activeGames = await IndexedDBService.getGamesByFilter('active');
|
||||
const completedGames = await IndexedDBService.getGamesByFilter('completed');
|
||||
console.log('✅ Filtering works - Active:', activeGames.length, 'Completed:', completedGames.length);
|
||||
|
||||
// Test 6: Test player statistics
|
||||
console.log('Test 6: Testing player statistics...');
|
||||
await IndexedDBService.updatePlayerStats('Test Player 1', 1, 3);
|
||||
await IndexedDBService.updatePlayerStats('Test Player 2', 1, 2);
|
||||
const playerHistory = await IndexedDBService.getPlayerNameHistory();
|
||||
console.log('✅ Player statistics updated:', playerHistory);
|
||||
|
||||
// Test 7: Test storage info
|
||||
console.log('Test 7: Testing storage information...');
|
||||
const storageInfo = await IndexedDBService.getStorageInfo();
|
||||
console.log('✅ Storage info:', storageInfo);
|
||||
|
||||
// Test 8: Test game deletion
|
||||
console.log('Test 8: Testing game deletion...');
|
||||
await IndexedDBService.deleteGame(testGame.id);
|
||||
const gamesAfterDelete = await IndexedDBService.loadGames();
|
||||
console.log('✅ Game deleted - Remaining games:', gamesAfterDelete.length);
|
||||
|
||||
console.log('🎉 All IndexedDB tests passed!');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'All tests passed successfully',
|
||||
storageInfo,
|
||||
playerHistory
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ IndexedDB test failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: 'Tests failed - check console for details'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test localStorage migration
|
||||
*/
|
||||
export async function testLocalStorageMigration() {
|
||||
console.log('🔄 Testing localStorage migration...');
|
||||
|
||||
try {
|
||||
// Create some test data in localStorage
|
||||
const testGames = [
|
||||
{
|
||||
id: Date.now(),
|
||||
gameType: '8-Ball',
|
||||
raceTo: 5,
|
||||
status: 'active',
|
||||
player1: 'Migration Test 1',
|
||||
player2: 'Migration Test 2',
|
||||
score1: 0,
|
||||
score2: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
log: [],
|
||||
undoStack: []
|
||||
}
|
||||
];
|
||||
|
||||
localStorage.setItem('bscscore_games', JSON.stringify(testGames));
|
||||
console.log('✅ Test data created in localStorage');
|
||||
|
||||
// Test migration
|
||||
const migratedGames = await GameService.loadGames();
|
||||
console.log('✅ Migration completed - Games loaded:', migratedGames.length);
|
||||
|
||||
// Verify localStorage is cleared
|
||||
const remainingData = localStorage.getItem('bscscore_games');
|
||||
console.log('✅ localStorage cleared after migration:', remainingData === null);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Migration test passed',
|
||||
migratedGames: migratedGames.length
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration test failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
message: 'Migration test failed'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Make functions available globally for console testing
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).testIndexedDB = testIndexedDB;
|
||||
(window as any).testLocalStorageMigration = testLocalStorageMigration;
|
||||
}
|
||||
@@ -27,39 +27,44 @@ export function validatePlayerName(name: string): ValidationResult {
|
||||
export function validateGameData(data: NewGameData): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate player names
|
||||
const player1Validation = validatePlayerName(data.player1);
|
||||
const player2Validation = validatePlayerName(data.player2);
|
||||
|
||||
errors.push(...player1Validation.errors);
|
||||
errors.push(...player2Validation.errors);
|
||||
try {
|
||||
// Validate player names
|
||||
const player1Validation = validatePlayerName(data.player1);
|
||||
const player2Validation = validatePlayerName(data.player2);
|
||||
|
||||
errors.push(...player1Validation.errors);
|
||||
errors.push(...player2Validation.errors);
|
||||
|
||||
// Check for duplicate player names
|
||||
const playerNames = [data.player1.trim(), data.player2.trim()];
|
||||
if (data.player3?.trim()) {
|
||||
const player3Validation = validatePlayerName(data.player3);
|
||||
errors.push(...player3Validation.errors);
|
||||
playerNames.push(data.player3.trim());
|
||||
}
|
||||
|
||||
const uniqueNames = new Set(playerNames.filter(name => name.length > 0));
|
||||
if (uniqueNames.size !== playerNames.filter(name => name.length > 0).length) {
|
||||
errors.push(VALIDATION_MESSAGES.DUPLICATE_PLAYER_NAMES);
|
||||
}
|
||||
|
||||
// Validate game type
|
||||
if (!data.gameType?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.GAME_TYPE_REQUIRED);
|
||||
}
|
||||
|
||||
// Validate race to
|
||||
if (!data.raceTo?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_REQUIRED);
|
||||
} else {
|
||||
const raceToNumber = parseInt(data.raceTo, 10);
|
||||
if (isNaN(raceToNumber) || raceToNumber <= 0) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_INVALID);
|
||||
// Check for duplicate player names
|
||||
const playerNames = [data.player1.trim(), data.player2.trim()];
|
||||
if (data.player3?.trim()) {
|
||||
const player3Validation = validatePlayerName(data.player3);
|
||||
errors.push(...player3Validation.errors);
|
||||
playerNames.push(data.player3.trim());
|
||||
}
|
||||
|
||||
const uniqueNames = new Set(playerNames.filter(name => name.length > 0));
|
||||
if (uniqueNames.size !== playerNames.filter(name => name.length > 0).length) {
|
||||
errors.push(VALIDATION_MESSAGES.DUPLICATE_PLAYER_NAMES);
|
||||
}
|
||||
|
||||
// Validate game type
|
||||
if (!data.gameType?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.GAME_TYPE_REQUIRED);
|
||||
}
|
||||
|
||||
// Validate race to
|
||||
if (!data.raceTo?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_REQUIRED);
|
||||
} else {
|
||||
const raceToNumber = parseInt(data.raceTo, 10);
|
||||
if (isNaN(raceToNumber) || raceToNumber <= 0) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_INVALID);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
errors.push('Ein unerwarteter Validierungsfehler ist aufgetreten');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user