mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
Refactor game logic into utility modules and add cross-device sync
Extracted game state management, share logic, and stats API calls into dedicated modules (game-persistence.svelte.ts, share.ts, stats-client.ts), and moved daily verse loading to client-side to fix timezone issues. Added a guesses column to daily_completions for cross-device state restoration for logged-in users, a new GET /api/stats endpoint, and a staging deploy script. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
148
src/lib/stores/game-persistence.svelte.ts
Normal file
148
src/lib/stores/game-persistence.svelte.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { evaluateGuess, generateUUID, type Guess } from "$lib/utils/game";
|
||||
|
||||
function getOrCreateAnonymousId(): string {
|
||||
if (!browser) return "";
|
||||
const key = "bibdle-anonymous-id";
|
||||
let id = localStorage.getItem(key);
|
||||
if (!id) {
|
||||
id = generateUUID();
|
||||
localStorage.setItem(key, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function createGamePersistence(
|
||||
getDate: () => string,
|
||||
getReference: () => string,
|
||||
getCorrectBookId: () => string,
|
||||
getUserId: () => string | undefined,
|
||||
) {
|
||||
let guesses = $state<Guess[]>([]);
|
||||
let anonymousId = $state("");
|
||||
let statsSubmitted = $state(false);
|
||||
let chapterGuessCompleted = $state(false);
|
||||
let chapterCorrect = $state(false);
|
||||
|
||||
// Initialize anonymous ID and load persisted flags
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const userId = getUserId();
|
||||
// CRITICAL: If user is logged in, ALWAYS use their user ID
|
||||
if (userId) {
|
||||
anonymousId = userId;
|
||||
} else {
|
||||
anonymousId = getOrCreateAnonymousId();
|
||||
}
|
||||
|
||||
if ((window as any).umami) {
|
||||
(window as any).umami.identify(anonymousId);
|
||||
}
|
||||
|
||||
const date = getDate();
|
||||
const reference = getReference();
|
||||
|
||||
statsSubmitted = localStorage.getItem(`bibdle-stats-submitted-${date}`) === "true";
|
||||
|
||||
const chapterGuessKey = `bibdle-chapter-guess-${reference}`;
|
||||
chapterGuessCompleted = localStorage.getItem(chapterGuessKey) !== null;
|
||||
if (chapterGuessCompleted) {
|
||||
const saved = localStorage.getItem(chapterGuessKey);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
const match = reference.match(/\s(\d+):/);
|
||||
const correctChapter = match ? parseInt(match[1], 10) : 1;
|
||||
chapterCorrect = data.selectedChapter === correctChapter;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load saved guesses from localStorage
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const date = getDate();
|
||||
const correctBookId = getCorrectBookId();
|
||||
const key = `bibdle-guesses-${date}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (!saved) {
|
||||
guesses = [];
|
||||
return;
|
||||
}
|
||||
|
||||
let savedIds: string[] = JSON.parse(saved);
|
||||
savedIds = Array.from(new Set(savedIds));
|
||||
guesses = savedIds
|
||||
.map((bookId) => evaluateGuess(bookId, correctBookId))
|
||||
.filter((g): g is Guess => g !== null);
|
||||
});
|
||||
|
||||
// Save guesses to localStorage whenever they change
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const date = getDate();
|
||||
localStorage.setItem(
|
||||
`bibdle-guesses-${date}`,
|
||||
JSON.stringify(guesses.map((g) => g.book.id)),
|
||||
);
|
||||
});
|
||||
|
||||
function markStatsSubmitted() {
|
||||
if (!browser) return;
|
||||
statsSubmitted = true;
|
||||
localStorage.setItem(`bibdle-stats-submitted-${getDate()}`, "true");
|
||||
}
|
||||
|
||||
function markWinTracked() {
|
||||
if (!browser) return;
|
||||
const key = `bibdle-win-tracked-${getDate()}`;
|
||||
if (localStorage.getItem(key) === "true") return false;
|
||||
localStorage.setItem(key, "true");
|
||||
return true;
|
||||
}
|
||||
|
||||
function isWinAlreadyTracked(): boolean {
|
||||
if (!browser) return false;
|
||||
return localStorage.getItem(`bibdle-win-tracked-${getDate()}`) === "true";
|
||||
}
|
||||
|
||||
function hydrateFromServer(guessIds: string[]) {
|
||||
if (!browser) return;
|
||||
const correctBookId = getCorrectBookId();
|
||||
const date = getDate();
|
||||
guesses = guessIds
|
||||
.map((bookId) => evaluateGuess(bookId, correctBookId))
|
||||
.filter((g): g is Guess => g !== null);
|
||||
// Persist to localStorage so subsequent loads on this device skip the server check
|
||||
localStorage.setItem(`bibdle-guesses-${date}`, JSON.stringify(guessIds));
|
||||
}
|
||||
|
||||
function onChapterGuessCompleted() {
|
||||
if (!browser) return;
|
||||
chapterGuessCompleted = true;
|
||||
const reference = getReference();
|
||||
const chapterGuessKey = `bibdle-chapter-guess-${reference}`;
|
||||
const saved = localStorage.getItem(chapterGuessKey);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
const match = reference.match(/\s(\d+):/);
|
||||
const correctChapter = match ? parseInt(match[1], 10) : 1;
|
||||
chapterCorrect = data.selectedChapter === correctChapter;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
get guesses() { return guesses; },
|
||||
set guesses(v: Guess[]) { guesses = v; },
|
||||
get anonymousId() { return anonymousId; },
|
||||
get statsSubmitted() { return statsSubmitted; },
|
||||
get chapterGuessCompleted() { return chapterGuessCompleted; },
|
||||
get chapterCorrect() { return chapterCorrect; },
|
||||
markStatsSubmitted,
|
||||
markWinTracked,
|
||||
isWinAlreadyTracked,
|
||||
onChapterGuessCompleted,
|
||||
hydrateFromServer,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user