mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
- Moved stats button, auth buttons, and debug info to bottom of main page - Added authentication requirement for /stats route - Show login prompt for unauthenticated users accessing stats - Include AuthModal for sign in/sign up from stats page 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
import { db } from '$lib/server/db';
|
|
import { dailyCompletions, type DailyCompletion } from '$lib/server/db/schema';
|
|
import { eq, desc } from 'drizzle-orm';
|
|
import type { PageServerLoad } from './$types';
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
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: []
|
|
},
|
|
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)
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).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)
|
|
}));
|
|
|
|
return {
|
|
stats: {
|
|
totalSolves,
|
|
avgGuesses,
|
|
gradeDistribution,
|
|
currentStreak,
|
|
bestStreak,
|
|
recentCompletions
|
|
},
|
|
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";
|
|
} |