feat: Remove localStorage for game states and implement cross-domain player ID sync

- Remove localStorage for game states and statistics (backend only)
- Add API route to suggest player ID based on recently updated states
- Add async player ID lookup that finds existing IDs across domains
- When visiting a new domain, automatically find and use existing player ID
- Enables cross-domain synchronization between hoerdle.de and hördle.de
This commit is contained in:
Hördle Bot
2025-12-01 20:37:47 +01:00
parent 27fa689b18
commit 2846afb6f7
4 changed files with 154 additions and 89 deletions

View File

@@ -28,9 +28,6 @@ export interface Statistics {
failed: number;
}
const STORAGE_KEY_PREFIX = 'hoerdle_game_state';
const STATS_KEY_PREFIX = 'hoerdle_statistics';
const INITIAL_SCORE = 90;
export function useGameState(
@@ -40,9 +37,6 @@ export function useGameState(
) {
const [gameState, setGameState] = useState<GameState | null>(null);
const [statistics, setStatistics] = useState<Statistics | null>(null);
const getStorageKey = () => genre ? `${STORAGE_KEY_PREFIX}_${genre}` : STORAGE_KEY_PREFIX;
const getStatsKey = () => genre ? `${STATS_KEY_PREFIX}_${genre}` : STATS_KEY_PREFIX;
// Get genre key for backend storage
// For specials, genre contains the special name
@@ -98,89 +92,31 @@ export function useGameState(
return;
}
} else {
// No backend state found - check localStorage before creating new state
const storageKey = getStorageKey();
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.date === today) {
// Migration for existing states without score
if (parsed.score === undefined) {
parsed.score = INITIAL_SCORE;
parsed.replayCount = 0;
parsed.skipCount = 0;
parsed.scoreBreakdown = [{ value: INITIAL_SCORE, reason: 'Start value' }];
parsed.yearGuessed = false;
}
setGameState(parsed as GameState);
// Also save to backend for cross-domain sync
const statsKey = getStatsKey();
const storedStats = localStorage.getItem(statsKey);
const stats = storedStats ? JSON.parse(storedStats) : createNewStatistics();
if (!statistics) {
setStatistics(stats);
}
await savePlayerState(genreKey, parsed as GameState, stats);
return;
}
}
// No state found - create new state
// No backend state found - create new state
// This is the normal case for first-time players or new genre
const newState = createNewState(today);
setGameState(newState);
localStorage.setItem(getStorageKey(), JSON.stringify(newState));
const newStats = createNewStatistics();
setStatistics(newStats);
localStorage.setItem(getStatsKey(), JSON.stringify(newStats));
// Save to backend for cross-domain sync
await savePlayerState(genreKey, newState, newStats);
return;
}
} catch (error) {
console.warn('[gameState] Failed to load from backend, falling back to localStorage:', error);
console.error('[gameState] Failed to load from backend:', error);
// Fallback to localStorage only on error
const storageKey = getStorageKey();
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.date === today) {
// Migration for existing states without score
if (parsed.score === undefined) {
parsed.score = INITIAL_SCORE;
parsed.replayCount = 0;
parsed.skipCount = 0;
parsed.scoreBreakdown = [{ value: INITIAL_SCORE, reason: 'Start value' }];
parsed.yearGuessed = false;
}
setGameState(parsed as GameState);
// Load statistics from localStorage
const statsKey = getStatsKey();
const storedStats = localStorage.getItem(statsKey);
if (storedStats) {
const parsedStats = JSON.parse(storedStats);
if (parsedStats.solvedIn7 === undefined) {
parsedStats.solvedIn7 = 0;
}
setStatistics(parsedStats);
} else {
const newStats = createNewStatistics();
setStatistics(newStats);
localStorage.setItem(statsKey, JSON.stringify(newStats));
}
return;
}
}
// No state found - create new state
// On error, create new state and try to save to backend
// This handles network errors gracefully
const newState = createNewState(today);
setGameState(newState);
localStorage.setItem(storageKey, JSON.stringify(newState));
const newStats = createNewStatistics();
setStatistics(newStats);
localStorage.setItem(getStatsKey(), JSON.stringify(newStats));
// Try to save to backend (may fail, but we try)
try {
await savePlayerState(genreKey, newState, newStats);
} catch (saveError) {
console.error('[gameState] Failed to save new state to backend:', saveError);
}
}
};
@@ -190,17 +126,15 @@ export function useGameState(
const saveState = async (newState: GameState) => {
setGameState(newState);
// Save to backend (primary)
// Save to backend only
if (statistics) {
try {
await savePlayerState(genreKey, newState, statistics);
} catch (error) {
console.warn('[gameState] Failed to save to backend, using localStorage fallback:', error);
console.error('[gameState] Failed to save to backend:', error);
// No fallback - backend is required for cross-domain sync
}
}
// Also save to localStorage as backup
localStorage.setItem(getStorageKey(), JSON.stringify(newState));
};
const updateStatistics = async (attempts: number, solved: boolean) => {
@@ -226,17 +160,15 @@ export function useGameState(
setStatistics(newStats);
// Save to backend (primary)
// Save to backend only
if (gameState) {
try {
await savePlayerState(genreKey, gameState, newStats);
} catch (error) {
console.warn('[gameState] Failed to save statistics to backend, using localStorage fallback:', error);
console.error('[gameState] Failed to save statistics to backend:', error);
// No fallback - backend is required for cross-domain sync
}
}
// Also save to localStorage as backup
localStorage.setItem(getStatsKey(), JSON.stringify(newStats));
};
const addGuess = (guess: string, correct: boolean) => {