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 <noreply@anthropic.com>
This commit is contained in:
George Powell
2026-02-11 13:01:53 -05:00
parent 78440cfbc3
commit 730b65201a
4 changed files with 248 additions and 100 deletions

View File

@@ -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<string, { count: number; totalGuesses: number }>();
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<string>();
const newTestamentBooks = new Set<string>();
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