From 730b65201a49abc51401d8da6c70fde572e3aaed Mon Sep 17 00:00:00 2001 From: George Powell Date: Wed, 11 Feb 2026 13:01:53 -0500 Subject: [PATCH] Redesign stats page with dark theme and enhanced statistics - Implement dark gradient background with glassmorphism cards - Add new statistics: worst day, best book, most seen book, unique books by testament - Design mobile-first responsive grid layout with optimized spacing - Update Container component to support dark theme (bg-white/10, border-white/20) - Calculate book-specific stats by linking completions to daily verses - Improve visual hierarchy with icons and color-coded stat cards Co-Authored-By: Claude Sonnet 4.5 --- src/lib/components/Container.svelte | 2 +- src/lib/utils/stats.ts | 15 ++ src/routes/stats/+page.server.ts | 87 +++++++++- src/routes/stats/+page.svelte | 244 +++++++++++++++++----------- 4 files changed, 248 insertions(+), 100 deletions(-) diff --git a/src/lib/components/Container.svelte b/src/lib/components/Container.svelte index b4d9674..42cc4a0 100644 --- a/src/lib/components/Container.svelte +++ b/src/lib/components/Container.svelte @@ -10,7 +10,7 @@
{@render children()}
diff --git a/src/lib/utils/stats.ts b/src/lib/utils/stats.ts index 7ce7a09..c6da4d8 100644 --- a/src/lib/utils/stats.ts +++ b/src/lib/utils/stats.ts @@ -18,6 +18,21 @@ export interface UserStats { guessCount: number; grade: string; }>; + worstDay: { + date: string; + guessCount: number; + } | null; + bestBook: { + bookId: string; + avgGuesses: number; + count: number; + } | null; + mostSeenBook: { + bookId: string; + count: number; + } | null; + totalBooksSeenOT: number; + totalBooksSeenNT: number; } export function getGradeColor(grade: string): string { diff --git a/src/routes/stats/+page.server.ts b/src/routes/stats/+page.server.ts index 976c78a..20c3004 100644 --- a/src/routes/stats/+page.server.ts +++ b/src/routes/stats/+page.server.ts @@ -1,7 +1,8 @@ import { db } from '$lib/server/db'; -import { dailyCompletions, type DailyCompletion } from '$lib/server/db/schema'; +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 @@ -51,7 +52,12 @@ export const load: PageServerLoad = async ({ url, locals }) => { }, currentStreak: 0, bestStreak: 0, - recentCompletions: [] + recentCompletions: [], + worstDay: null, + bestBook: null, + mostSeenBook: null, + totalBooksSeenOT: 0, + totalBooksSeenNT: 0 }, user: locals.user, session: locals.session @@ -133,6 +139,66 @@ export const load: PageServerLoad = async ({ url, locals }) => { 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, @@ -140,7 +206,22 @@ export const load: PageServerLoad = async ({ url, locals }) => { gradeDistribution, currentStreak, bestStreak, - recentCompletions + 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 diff --git a/src/routes/stats/+page.svelte b/src/routes/stats/+page.svelte index 66c51aa..67a3ce5 100644 --- a/src/routes/stats/+page.svelte +++ b/src/routes/stats/+page.svelte @@ -4,12 +4,14 @@ import { onMount } from "svelte"; import { enhance } from '$app/forms'; import AuthModal from "$lib/components/AuthModal.svelte"; - import { - getGradeColor, - formatDate, - getStreakMessage, + import Container from "$lib/components/Container.svelte"; + import { bibleBooks } from "$lib/types/bible"; + import { + getGradeColor, + formatDate, + getStreakMessage, getPerformanceMessage, - type UserStats + type UserStats } from "$lib/utils/stats"; interface PageData { @@ -44,6 +46,10 @@ return total > 0 ? Math.round((count / total) * 100) : 0; } + function getBookName(bookId: string): string { + return bibleBooks.find(b => b.id === bookId)?.name || bookId; + } + $inspect(data); @@ -52,32 +58,30 @@ -
-
+
+
-
-

Your Stats

-

Track your Bibdle performance over time

- +
+

Your Stats

+

Track your Bibdle performance over time

+ + ← Back to Game +
{#if loading}
-

Loading your stats...

+

Loading your stats...

{:else if data.requiresAuth}
-
-

Authentication Required

-

You must be logged in to see your stats.

+
+

Authentication Required

+

You must be logged in to see your stats.

- ← Back to Game @@ -96,10 +100,10 @@
{: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.currentStreak}
+
Current Streak
-
+ + + + +
+
+
{stats.bestStreak}
+
Best Streak
+
+
-
+
-
{stats.avgGuesses}
-
Avg. Guesses
-
per solve
+
🎯
+
{stats.avgGuesses}
+
Avg Guesses
-
+ - -
+ +
-
{stats.currentStreak}
-
Current Streak
-
- {getStreakMessage(stats.currentStreak)} -
+
+
{stats.totalSolves}
+
Total Solves
-
+
- {#if stats.totalSolves > 0} -
-

Grade Distribution

-
- {#each Object.entries(stats.gradeDistribution) as [grade, count]} + +
+ + {#if stats.worstDay} + +
+
😅
+
+
Worst Day
+
{stats.worstDay.guessCount} guesses
+
{formatDate(stats.worstDay.date)}
+
+
+
+ {/if} + + + {#if stats.bestBook} + +
+
🏆
+
+
Best Book
+
{getBookName(stats.bestBook.bookId)}
+
{stats.bestBook.avgGuesses} avg guesses ({stats.bestBook.count}x)
+
+
+
+ {/if} + + + {#if stats.mostSeenBook} + +
+
📖
+
+
Most Seen Book
+
{getBookName(stats.mostSeenBook.bookId)}
+
{stats.mostSeenBook.count} time{stats.mostSeenBook.count === 1 ? '' : 's'}
+
+
+
+ {/if} + + + +
+
📚
+
+
Unique Books
+
+ {stats.totalBooksSeenOT + stats.totalBooksSeenNT} +
+
OT: {stats.totalBooksSeenOT} / NT: {stats.totalBooksSeenNT}
+
+
+
+
+ + + +

Grade Distribution

+
+ {#each Object.entries(stats.gradeDistribution) as [grade, count] (grade)} {@const percentage = getGradePercentage(count, stats.totalSolves)}
- + {grade}
-
{count}
-
{percentage}%
+
{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} -
+ +

Recent Performance

+
+ {#each stats.recentCompletions as completion (completion.date)} +
- {formatDate(completion.date)} + {formatDate(completion.date)}
-
- {completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'} - +
+ {completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'} + {completion.grade}
{/each}
-
+
{/if} {/if} {/if}
- \ No newline at end of file + \ No newline at end of file