diff --git a/src/lib/utils/stats.ts b/src/lib/utils/stats.ts
new file mode 100644
index 0000000..7ce7a09
--- /dev/null
+++ b/src/lib/utils/stats.ts
@@ -0,0 +1,71 @@
+export interface UserStats {
+ totalSolves: number;
+ avgGuesses: number;
+ gradeDistribution: {
+ 'S++': number;
+ 'S+': number;
+ 'A+': number;
+ 'A': number;
+ 'B+': number;
+ 'B': number;
+ 'C+': number;
+ 'C': number;
+ };
+ currentStreak: number;
+ bestStreak: number;
+ recentCompletions: Array<{
+ date: string;
+ guessCount: number;
+ grade: string;
+ }>;
+}
+
+export function getGradeColor(grade: string): string {
+ switch (grade) {
+ case 'S++': return 'text-purple-600 bg-purple-100';
+ case 'S+': return 'text-yellow-600 bg-yellow-100';
+ case 'A+': return 'text-green-600 bg-green-100';
+ case 'A': return 'text-green-500 bg-green-50';
+ case 'B+': return 'text-blue-600 bg-blue-100';
+ case 'B': return 'text-blue-500 bg-blue-50';
+ case 'C+': return 'text-orange-600 bg-orange-100';
+ case 'C': return 'text-red-600 bg-red-100';
+ default: return 'text-gray-600 bg-gray-100';
+ }
+}
+
+export function formatDate(dateStr: string): string {
+ const date = new Date(dateStr + 'T00:00:00');
+ return date.toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric'
+ });
+}
+
+export function getStreakMessage(currentStreak: number): string {
+ if (currentStreak === 0) {
+ return "Start your streak today!";
+ } else if (currentStreak === 1) {
+ return "Keep it going!";
+ } else if (currentStreak < 7) {
+ return `${currentStreak} days strong!`;
+ } else if (currentStreak < 30) {
+ return `${currentStreak} day streak - amazing!`;
+ } else {
+ return `${currentStreak} days - you're unstoppable!`;
+ }
+}
+
+export function getPerformanceMessage(avgGuesses: number): string {
+ if (avgGuesses <= 2) {
+ return "Exceptional performance!";
+ } else if (avgGuesses <= 4) {
+ return "Great performance!";
+ } else if (avgGuesses <= 6) {
+ return "Good performance!";
+ } else if (avgGuesses <= 8) {
+ return "Room for improvement!";
+ } else {
+ return "Keep practicing!";
+ }
+}
\ No newline at end of file
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index ffcdd8f..64aa6a0 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -447,6 +447,14 @@
{isDev ? "Dev Edition | " : ""}{currentDate}
+
diff --git a/src/routes/stats/+page.server.ts b/src/routes/stats/+page.server.ts
new file mode 100644
index 0000000..d263139
--- /dev/null
+++ b/src/routes/stats/+page.server.ts
@@ -0,0 +1,149 @@
+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 }) => {
+ const anonymousId = url.searchParams.get('anonymousId');
+
+ if (!anonymousId) {
+ return {
+ stats: null,
+ error: 'No anonymous ID provided'
+ };
+ }
+
+ try {
+ // Get all completions for this user
+ const completions = await db
+ .select()
+ .from(dailyCompletions)
+ .where(eq(dailyCompletions.anonymousId, anonymousId))
+ .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: []
+ }
+ };
+ }
+
+ // 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
+ }
+ };
+
+ } catch (error) {
+ console.error('Error fetching user stats:', error);
+ return {
+ stats: null,
+ error: 'Failed to fetch stats'
+ };
+ }
+};
+
+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";
+}
\ No newline at end of file
diff --git a/src/routes/stats/+page.svelte b/src/routes/stats/+page.svelte
new file mode 100644
index 0000000..a0753d8
--- /dev/null
+++ b/src/routes/stats/+page.svelte
@@ -0,0 +1,206 @@
+
+
+
+ Stats | Bibdle
+
+
+
+
+
+
+
+
Your Stats
+
Track your Bibdle performance over time
+
+
+
+ {#if loading}
+
+
+
Loading your stats...
+
+ {:else if data.error}
+
+ {:else if !data.stats}
+
+ {:else}
+ {@const stats = data.stats}
+
+
+
+
+
+
+
{stats.totalSolves}
+
Total Solves
+ {#if stats.totalSolves > 0}
+
+ {getPerformanceMessage(stats.avgGuesses)}
+
+ {/if}
+
+
+
+
+
+
+
{stats.avgGuesses}
+
Avg. Guesses
+
per solve
+
+
+
+
+
+
+
{stats.currentStreak}
+
Current Streak
+
+ {getStreakMessage(stats.currentStreak)}
+
+
+
+
+
+
+ {#if stats.totalSolves > 0}
+
+
Grade Distribution
+
+ {#each Object.entries(stats.gradeDistribution) as [grade, count]}
+ {@const percentage = getGradePercentage(count, stats.totalSolves)}
+
+
+
+ {grade}
+
+
+
{count}
+
{percentage}%
+
+ {/each}
+
+
+
+
+
+
Streak Information
+
+
+
{stats.currentStreak}
+
Current Streak
+
+
+
{stats.bestStreak}
+
Best Streak
+
+
+
+
+
+ {#if stats.recentCompletions.length > 0}
+
+
Recent Performance
+
+ {#each stats.recentCompletions as completion}
+
+
+ {formatDate(completion.date)}
+
+
+ {completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}
+
+ {completion.grade}
+
+
+
+ {/each}
+
+
+ {/if}
+ {/if}
+ {/if}
+
+
\ No newline at end of file