feat: Add backend storage for cross-domain player state synchronization
- Add PlayerState model to database schema for storing game states - Create player identifier system (UUID-based) for cross-domain sync - Implement API endpoints for loading/saving player states - Refactor gameState hook to use backend storage with localStorage fallback - Support synchronization between hoerdle.de and hördle.de - Migration automatically runs on Docker container start
This commit is contained in:
130
lib/playerStorage.ts
Normal file
130
lib/playerStorage.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Player Storage API
|
||||
*
|
||||
* Handles loading and saving player game states from/to the backend.
|
||||
* This enables cross-domain synchronization between hoerdle.de and hördle.de.
|
||||
*/
|
||||
|
||||
import { getOrCreatePlayerId } from './playerId';
|
||||
import type { GameState, Statistics } from './gameState';
|
||||
|
||||
/**
|
||||
* Get genre key for storage
|
||||
*
|
||||
* Formats the genre/special into a consistent key format:
|
||||
* - Global: "global"
|
||||
* - Genre: "Rock" (localized name)
|
||||
* - Special: "special:00725" (special name)
|
||||
*/
|
||||
export function getGenreKey(
|
||||
genre: string | null,
|
||||
isSpecial: boolean,
|
||||
specialName?: string
|
||||
): string {
|
||||
if (isSpecial && specialName) {
|
||||
return `special:${specialName}`;
|
||||
}
|
||||
return genre || 'global';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load player state from backend
|
||||
*
|
||||
* @param genreKey - Genre key (from getGenreKey)
|
||||
* @returns GameState and Statistics, or null if not found
|
||||
*/
|
||||
export async function loadPlayerState(
|
||||
genreKey: string
|
||||
): Promise<{ gameState: GameState; statistics: Statistics } | null> {
|
||||
try {
|
||||
const playerId = getOrCreatePlayerId();
|
||||
if (!playerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine if it's a special or genre
|
||||
const isSpecial = genreKey.startsWith('special:');
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (isSpecial) {
|
||||
const specialName = genreKey.replace('special:', '');
|
||||
params.append('special', specialName);
|
||||
} else if (genreKey !== 'global') {
|
||||
params.append('genre', genreKey);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/player-state?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Player-Id': playerId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// No state found - this is normal for new players
|
||||
return null;
|
||||
}
|
||||
// Other errors: log and return null (will fallback to localStorage)
|
||||
console.warn('[playerStorage] Failed to load player state:', response.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data || !data.gameState || !data.statistics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
gameState: data.gameState as GameState,
|
||||
statistics: data.statistics as Statistics,
|
||||
};
|
||||
} catch (error) {
|
||||
// Network errors or other issues: fallback to localStorage
|
||||
console.warn('[playerStorage] Error loading player state:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save player state to backend
|
||||
*
|
||||
* @param genreKey - Genre key (from getGenreKey)
|
||||
* @param gameState - Current game state
|
||||
* @param statistics - Current statistics
|
||||
*/
|
||||
export async function savePlayerState(
|
||||
genreKey: string,
|
||||
gameState: GameState,
|
||||
statistics: Statistics
|
||||
): Promise<void> {
|
||||
try {
|
||||
const playerId = getOrCreatePlayerId();
|
||||
if (!playerId) {
|
||||
console.warn('[playerStorage] No player ID available, cannot save state');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/player-state', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Player-Id': playerId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
genreKey,
|
||||
gameState,
|
||||
statistics,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[playerStorage] Failed to save player state:', response.status);
|
||||
// Don't throw - allow fallback to localStorage
|
||||
}
|
||||
} catch (error) {
|
||||
// Network errors: log but don't throw (will fallback to localStorage)
|
||||
console.warn('[playerStorage] Error saving player state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user