mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-08-22 22:32:28 -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:
@@ -7,8 +7,11 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const { date } = body;
|
||||
|
||||
// Use the date provided by the client (already calculated in their timezone)
|
||||
const dateStr = date || new Date().toISOString().split('T')[0];
|
||||
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
return json({ error: 'A valid date (YYYY-MM-DD) is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const dateStr = date;
|
||||
|
||||
const dailyVerse = await getVerseForDate(dateStr);
|
||||
const correctBook = getBookById(dailyVerse.bookId) ?? null;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { dailyCompletions } from '$lib/server/db/schema';
|
||||
import { and, eq, asc } from 'drizzle-orm';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
try {
|
||||
const anonymousId = url.searchParams.get('anonymousId');
|
||||
const date = url.searchParams.get('date');
|
||||
|
||||
if (!anonymousId || !date) {
|
||||
return json({ error: 'Invalid data' }, { status: 400 });
|
||||
}
|
||||
|
||||
const userCompletions = await db
|
||||
.select()
|
||||
.from(dailyCompletions)
|
||||
.where(and(
|
||||
eq(dailyCompletions.anonymousId, anonymousId),
|
||||
eq(dailyCompletions.date, date)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (userCompletions.length === 0) {
|
||||
return json({ error: 'No completion found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const userCompletion = userCompletions[0];
|
||||
const guessCount = userCompletion.guessCount;
|
||||
|
||||
const allCompletions = await db
|
||||
.select()
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.date, date))
|
||||
.orderBy(asc(dailyCompletions.completedAt));
|
||||
|
||||
const totalSolves = allCompletions.length;
|
||||
|
||||
const solveRank = allCompletions.findIndex(c => c.anonymousId === anonymousId) + 1;
|
||||
|
||||
const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length;
|
||||
const guessRank = betterGuesses + 1;
|
||||
|
||||
const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length;
|
||||
|
||||
const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0);
|
||||
const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10;
|
||||
|
||||
const betterOrEqualCount = allCompletions.filter(c => c.guessCount <= guessCount).length;
|
||||
const percentile = Math.round((betterOrEqualCount / totalSolves) * 100);
|
||||
|
||||
const guesses = userCompletion.guesses ? JSON.parse(userCompletion.guesses) : undefined;
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount, percentile, guesses }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
return json({ error: 'Failed to fetch stats' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { db } from '$lib/server/db';
|
||||
import { dailyCompletions } from '$lib/server/db/schema';
|
||||
import { and, eq, asc } from 'drizzle-orm';
|
||||
import { eq, asc } from 'drizzle-orm';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
const { anonymousId, date, guessCount } = await request.json();
|
||||
const { anonymousId, date, guessCount, guesses } = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!anonymousId || !date || typeof guessCount !== 'number' || guessCount < 1) {
|
||||
@@ -23,6 +23,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
anonymousId,
|
||||
date,
|
||||
guessCount,
|
||||
guesses: Array.isArray(guesses) ? JSON.stringify(guesses) : null,
|
||||
completedAt,
|
||||
});
|
||||
} catch (err: any) {
|
||||
@@ -68,67 +69,3 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
return json({ error: 'Failed to submit completion' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
try {
|
||||
const anonymousId = url.searchParams.get('anonymousId');
|
||||
const date = url.searchParams.get('date');
|
||||
|
||||
if (!anonymousId || !date) {
|
||||
return json({ error: 'Invalid data' }, { status: 400 });
|
||||
}
|
||||
|
||||
const userCompletions = await db
|
||||
.select()
|
||||
.from(dailyCompletions)
|
||||
.where(and(
|
||||
eq(dailyCompletions.anonymousId, anonymousId),
|
||||
eq(dailyCompletions.date, date)
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (userCompletions.length === 0) {
|
||||
return json({ error: 'No completion found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const userCompletion = userCompletions[0];
|
||||
const guessCount = userCompletion.guessCount;
|
||||
|
||||
// Calculate statistics
|
||||
const allCompletions = await db
|
||||
.select()
|
||||
.from(dailyCompletions)
|
||||
.where(eq(dailyCompletions.date, date))
|
||||
.orderBy(asc(dailyCompletions.completedAt));
|
||||
|
||||
const totalSolves = allCompletions.length;
|
||||
|
||||
// Solve rank: position in time-ordered list
|
||||
const solveRank = allCompletions.findIndex(c => c.anonymousId === anonymousId) + 1;
|
||||
|
||||
// Guess rank: count how many had FEWER guesses (ties get same rank)
|
||||
const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length;
|
||||
const guessRank = betterGuesses + 1;
|
||||
|
||||
// Count ties: how many have the SAME guessCount (excluding self)
|
||||
const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length;
|
||||
|
||||
// Average guesses
|
||||
const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0);
|
||||
const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10;
|
||||
|
||||
// Percentile: what percentage of people you beat (100 - your rank percentage)
|
||||
const betterOrEqualCount = allCompletions.filter(c => c.guessCount <= guessCount).length;
|
||||
const percentile = Math.round((betterOrEqualCount / totalSolves) * 100);
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount, percentile }
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
return json({ error: 'Failed to fetch stats' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user