import { db } from '$lib/server/db'; import { dailyCompletions, dailyVerses, type DailyCompletion } from '$lib/server/db/schema'; import { eq, desc } from 'drizzle-orm'; import type { PageServerLoad } from './$types'; import { bibleBooks } from '$lib/types/bible'; export const load: PageServerLoad = async ({ url, locals }) => { // Check if user is authenticated if (!locals.user) { return { stats: null, error: null, user: null, session: null, requiresAuth: true }; } const userId = locals.user.id; if (!userId) { return { stats: null, error: 'No user ID provided', user: locals.user, session: locals.session }; } // Get user's current date from timezone query param const timezone = url.searchParams.get('tz') || 'UTC'; const userToday = new Date().toLocaleDateString('en-CA', { timeZone: timezone }); try { // Get all completions for this user const completions = await db .select() .from(dailyCompletions) .where(eq(dailyCompletions.anonymousId, userId)) .orderBy(desc(dailyCompletions.date)); if (completions.length === 0) { return { stats: { totalSolves: 0, avgGuesses: 0, gradeDistribution: { 'S++': 0, 'S+': 0, 'A+': 0, 'A': 0, 'B+': 0, 'B': 0, 'C+': 0, 'C': 0 }, currentStreak: 0, bestStreak: 0, recentCompletions: [], worstDay: null, bestBook: null, mostSeenBook: null, totalBooksSeenOT: 0, totalBooksSeenNT: 0 }, user: locals.user, session: locals.session }; } // Calculate basic stats const totalSolves = completions.length; const totalGuesses = completions.reduce((sum: number, c: DailyCompletion) => sum + c.guessCount, 0); const avgGuesses = Math.round((totalGuesses / totalSolves) * 100) / 100; // Calculate grade distribution const gradeDistribution = { 'S++': 0, // This will be calculated differently since we don't store chapter correctness 'S+': completions.filter((c: DailyCompletion) => c.guessCount === 1).length, 'A+': completions.filter((c: DailyCompletion) => c.guessCount === 2).length, 'A': completions.filter((c: DailyCompletion) => c.guessCount === 3).length, 'B+': completions.filter((c: DailyCompletion) => c.guessCount >= 4 && c.guessCount <= 6).length, 'B': completions.filter((c: DailyCompletion) => c.guessCount >= 7 && c.guessCount <= 10).length, 'C+': completions.filter((c: DailyCompletion) => c.guessCount >= 11 && c.guessCount <= 15).length, 'C': completions.filter((c: DailyCompletion) => c.guessCount > 15).length }; // Calculate streaks const sortedDates = completions .map((c: DailyCompletion) => c.date) .sort(); let currentStreak = 0; let bestStreak = 0; let tempStreak = 1; if (sortedDates.length > 0) { // Check if current streak is active (includes today or yesterday) // Use the user's local date passed from the client const today = userToday; const yesterdayDate = new Date(userToday); yesterdayDate.setDate(yesterdayDate.getDate() - 1); const yesterday = yesterdayDate.toISOString().split('T')[0]; const lastPlayedDate = sortedDates[sortedDates.length - 1]; if (lastPlayedDate === today || lastPlayedDate === yesterday) { currentStreak = 1; // Count backwards from the most recent date for (let i = sortedDates.length - 2; i >= 0; i--) { const currentDate = new Date(sortedDates[i + 1]); const prevDate = new Date(sortedDates[i]); const daysDiff = Math.floor((currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff === 1) { currentStreak++; } else { break; } } } // Calculate best streak bestStreak = 1; for (let i = 1; i < sortedDates.length; i++) { const currentDate = new Date(sortedDates[i]); const prevDate = new Date(sortedDates[i - 1]); const daysDiff = Math.floor((currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff === 1) { tempStreak++; } else { bestStreak = Math.max(bestStreak, tempStreak); tempStreak = 1; } } bestStreak = Math.max(bestStreak, tempStreak); } // Get recent completions (last 7 days) const recentCompletions = completions .slice(0, 7) .map((c: DailyCompletion) => ({ date: c.date, guessCount: c.guessCount, grade: getGradeFromGuesses(c.guessCount) })); // Calculate worst day (highest guess count) const worstDay = completions.reduce((max, c) => c.guessCount > max.guessCount ? c : max, completions[0] ); // Get all daily verses to link completions to books const allVerses = await db .select() .from(dailyVerses); // Create a map of date -> bookId const dateToBookId = new Map(allVerses.map(v => [v.date, v.bookId])); // Calculate book-specific stats const bookStats = new Map(); for (const completion of completions) { const bookId = dateToBookId.get(completion.date); if (bookId) { const existing = bookStats.get(bookId) || { count: 0, totalGuesses: 0 }; bookStats.set(bookId, { count: existing.count + 1, totalGuesses: existing.totalGuesses + completion.guessCount }); } } // Find book you know the best (lowest avg guesses) let bestBook: { bookId: string; avgGuesses: number; count: number } | null = null; for (const [bookId, stats] of bookStats.entries()) { const avgGuesses = stats.totalGuesses / stats.count; if (!bestBook || avgGuesses < bestBook.avgGuesses) { bestBook = { bookId, avgGuesses, count: stats.count }; } } // Find most seen book let mostSeenBook: { bookId: string; count: number } | null = null; for (const [bookId, stats] of bookStats.entries()) { if (!mostSeenBook || stats.count > mostSeenBook.count) { mostSeenBook = { bookId, count: stats.count }; } } // Count unique books by testament const oldTestamentBooks = new Set(); const newTestamentBooks = new Set(); for (const [bookId, _] of bookStats.entries()) { const book = bibleBooks.find(b => b.id === bookId); if (book) { if (book.testament === 'old') { oldTestamentBooks.add(bookId); } else { newTestamentBooks.add(bookId); } } } return { stats: { totalSolves, avgGuesses, gradeDistribution, currentStreak, bestStreak, recentCompletions, worstDay: { date: worstDay.date, guessCount: worstDay.guessCount }, bestBook: bestBook ? { bookId: bestBook.bookId, avgGuesses: Math.round(bestBook.avgGuesses * 100) / 100, count: bestBook.count } : null, mostSeenBook: mostSeenBook ? { bookId: mostSeenBook.bookId, count: mostSeenBook.count } : null, totalBooksSeenOT: oldTestamentBooks.size, totalBooksSeenNT: newTestamentBooks.size }, user: locals.user, session: locals.session }; } catch (error) { console.error('Error fetching user stats:', error); return { stats: null, error: 'Failed to fetch stats', user: locals.user, session: locals.session }; } }; function getGradeFromGuesses(guessCount: number): string { if (guessCount === 1) return "S+"; if (guessCount === 2) return "A+"; if (guessCount === 3) return "A"; if (guessCount >= 4 && guessCount <= 6) return "B+"; if (guessCount >= 7 && guessCount <= 10) return "B"; if (guessCount >= 11 && guessCount <= 15) return "C+"; return "C"; }