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

@@ -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);
</script>
@@ -52,32 +58,30 @@
<meta name="description" content="View your Bibdle game statistics and performance" />
</svelte:head>
<div class="min-h-screen bg-gradient-to-br from-amber-50 to-orange-100 p-4">
<div class="max-w-4xl mx-auto">
<div class="min-h-screen bg-gradient-to-br from-gray-900 via-slate-900 to-gray-900 p-4 md:p-8">
<div class="max-w-6xl mx-auto">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">Your Stats</h1>
<p class="text-gray-600">Track your Bibdle performance over time</p>
<div class="mt-4">
<a
href="/"
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
>
← Back to Game
</a>
</div>
<div class="text-center mb-6 md:mb-8">
<h1 class="text-3xl md:text-4xl font-bold text-gray-100 mb-2">Your Stats</h1>
<p class="text-sm md:text-base text-gray-300 mb-4">Track your Bibdle performance over time</p>
<a
href="/"
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium shadow-md"
>
← Back to Game
</a>
</div>
{#if loading}
<div class="text-center py-12">
<div class="inline-block w-8 h-8 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div>
<p class="mt-4 text-gray-600">Loading your stats...</p>
<p class="mt-4 text-gray-300">Loading your stats...</p>
</div>
{:else if data.requiresAuth}
<div class="text-center py-12">
<div class="bg-blue-100 border border-blue-300 rounded-lg p-8 max-w-md mx-auto">
<h2 class="text-2xl font-bold text-blue-800 mb-4">Authentication Required</h2>
<p class="text-blue-700 mb-6">You must be logged in to see your stats.</p>
<div class="bg-blue-950/50 border border-blue-800/50 rounded-lg p-8 max-w-md mx-auto backdrop-blur-sm">
<h2 class="text-2xl font-bold text-blue-200 mb-4">Authentication Required</h2>
<p class="text-blue-300 mb-6">You must be logged in to see your stats.</p>
<div class="flex flex-col gap-3">
<button
onclick={() => authModalOpen = true}
@@ -85,9 +89,9 @@
>
🔐 Sign In / Sign Up
</button>
<a
href="/"
class="inline-flex items-center justify-center px-6 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors font-medium"
<a
href="/"
class="inline-flex items-center justify-center px-6 py-3 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors font-medium"
>
← Back to Game
</a>
@@ -96,10 +100,10 @@
</div>
{:else if data.error}
<div class="text-center py-12">
<div class="bg-red-100 border border-red-300 rounded-lg p-6 max-w-md mx-auto">
<p class="text-red-700">{data.error}</p>
<a
href="/"
<div class="bg-red-950/50 border border-red-800/50 rounded-lg p-6 max-w-md mx-auto backdrop-blur-sm">
<p class="text-red-300">{data.error}</p>
<a
href="/"
class="mt-4 inline-block px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
>
Return to Game
@@ -108,114 +112,162 @@
</div>
{:else if !data.stats}
<div class="text-center py-12">
<div class="bg-yellow-100 border border-yellow-300 rounded-lg p-6 max-w-md mx-auto">
<p class="text-yellow-700">No stats available.</p>
<a
href="/"
class="mt-4 inline-block px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 transition-colors"
<Container class="p-8 max-w-md mx-auto">
<div class="text-yellow-400 mb-4 text-lg">No stats available yet.</div>
<p class="text-gray-300 mb-6">Start playing to build your stats!</p>
<a
href="/"
class="inline-flex items-center px-6 py-2.5 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors font-medium shadow-md"
>
Start Playing
</a>
</div>
</Container>
</div>
{:else}
{@const stats = data.stats}
<!-- Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Total Solves -->
<div class="bg-white rounded-lg shadow-md p-6">
<!-- Key Stats Grid -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-6">
<!-- Current Streak -->
<Container class="p-4 md:p-6">
<div class="text-center">
<div class="text-3xl font-bold text-amber-600 mb-2">{stats.totalSolves}</div>
<div class="text-gray-600">Total Solves</div>
{#if stats.totalSolves > 0}
<div class="text-sm text-gray-500 mt-1">
{getPerformanceMessage(stats.avgGuesses)}
</div>
{/if}
<div class="text-2xl md:text-3xl mb-1">🔥</div>
<div class="text-2xl md:text-3xl font-bold text-orange-400 mb-1">{stats.currentStreak}</div>
<div class="text-xs md:text-sm text-gray-300 font-medium">Current Streak</div>
</div>
</div>
</Container>
<!-- Longest Streak -->
<Container class="p-4 md:p-6">
<div class="text-center">
<div class="text-2xl md:text-3xl mb-1"></div>
<div class="text-2xl md:text-3xl font-bold text-purple-400 mb-1">{stats.bestStreak}</div>
<div class="text-xs md:text-sm text-gray-300 font-medium">Best Streak</div>
</div>
</Container>
<!-- Average Guesses -->
<div class="bg-white rounded-lg shadow-md p-6">
<Container class="p-4 md:p-6">
<div class="text-center">
<div class="text-3xl font-bold text-blue-600 mb-2">{stats.avgGuesses}</div>
<div class="text-gray-600">Avg. Guesses</div>
<div class="text-sm text-gray-500 mt-1">per solve</div>
<div class="text-2xl md:text-3xl mb-1">🎯</div>
<div class="text-2xl md:text-3xl font-bold text-blue-400 mb-1">{stats.avgGuesses}</div>
<div class="text-xs md:text-sm text-gray-300 font-medium">Avg Guesses</div>
</div>
</div>
</Container>
<!-- Current Streak -->
<div class="bg-white rounded-lg shadow-md p-6">
<!-- Total Solves -->
<Container class="p-4 md:p-6">
<div class="text-center">
<div class="text-3xl font-bold text-green-600 mb-2">{stats.currentStreak}</div>
<div class="text-gray-600">Current Streak</div>
<div class="text-sm text-gray-500 mt-1">
{getStreakMessage(stats.currentStreak)}
</div>
<div class="text-2xl md:text-3xl mb-1"></div>
<div class="text-2xl md:text-3xl font-bold text-green-400 mb-1">{stats.totalSolves}</div>
<div class="text-xs md:text-sm text-gray-300 font-medium">Total Solves</div>
</div>
</div>
</Container>
</div>
<!-- Grade Distribution -->
{#if stats.totalSolves > 0}
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-2xl font-bold text-gray-800 mb-4">Grade Distribution</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
{#each Object.entries(stats.gradeDistribution) as [grade, count]}
<!-- Book Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4 mb-6">
<!-- Worst Day -->
{#if stats.worstDay}
<Container class="p-4 md:p-6">
<div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">😅</div>
<div class="flex-1 min-w-0">
<div class="text-sm md:text-base text-gray-300 font-medium mb-1">Worst Day</div>
<div class="text-xl md:text-2xl font-bold text-red-400 truncate">{stats.worstDay.guessCount} guesses</div>
<div class="text-xs md:text-sm text-gray-400">{formatDate(stats.worstDay.date)}</div>
</div>
</div>
</Container>
{/if}
<!-- Best Book -->
{#if stats.bestBook}
<Container class="p-4 md:p-6">
<div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">🏆</div>
<div class="flex-1 min-w-0">
<div class="text-sm md:text-base text-gray-300 font-medium mb-1">Best Book</div>
<div class="text-lg md:text-xl font-bold text-amber-400 truncate">{getBookName(stats.bestBook.bookId)}</div>
<div class="text-xs md:text-sm text-gray-400">{stats.bestBook.avgGuesses} avg guesses ({stats.bestBook.count}x)</div>
</div>
</div>
</Container>
{/if}
<!-- Most Seen Book -->
{#if stats.mostSeenBook}
<Container class="p-4 md:p-6">
<div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">📖</div>
<div class="flex-1 min-w-0">
<div class="text-sm md:text-base text-gray-300 font-medium mb-1">Most Seen Book</div>
<div class="text-lg md:text-xl font-bold text-indigo-400 truncate">{getBookName(stats.mostSeenBook.bookId)}</div>
<div class="text-xs md:text-sm text-gray-400">{stats.mostSeenBook.count} time{stats.mostSeenBook.count === 1 ? '' : 's'}</div>
</div>
</div>
</Container>
{/if}
<!-- Total Books Seen -->
<Container class="p-4 md:p-6">
<div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">📚</div>
<div class="flex-1 min-w-0">
<div class="text-sm md:text-base text-gray-300 font-medium mb-1">Unique Books</div>
<div class="text-xl md:text-2xl font-bold text-teal-400">
{stats.totalBooksSeenOT + stats.totalBooksSeenNT}
</div>
<div class="text-xs md:text-sm text-gray-400">OT: {stats.totalBooksSeenOT} / NT: {stats.totalBooksSeenNT}</div>
</div>
</div>
</Container>
</div>
<!-- Grade Distribution -->
<Container class="p-5 md:p-6 mb-6">
<h2 class="text-lg md:text-xl font-bold text-gray-100 mb-4">Grade Distribution</h2>
<div class="grid grid-cols-4 md:grid-cols-8 gap-2 md:gap-3">
{#each Object.entries(stats.gradeDistribution) as [grade, count] (grade)}
{@const percentage = getGradePercentage(count, stats.totalSolves)}
<div class="text-center">
<div class="mb-2">
<span class="inline-block px-3 py-1 rounded-full text-sm font-semibold {getGradeColor(grade)}">
<span class="inline-block px-2 md:px-3 py-1 rounded-full text-xs md:text-sm font-semibold {getGradeColor(grade)}">
{grade}
</span>
</div>
<div class="text-2xl font-bold text-gray-800">{count}</div>
<div class="text-sm text-gray-500">{percentage}%</div>
<div class="text-lg md:text-2xl font-bold text-gray-100">{count}</div>
<div class="text-xs text-gray-400">{percentage}%</div>
</div>
{/each}
</div>
</div>
<!-- Streak Info -->
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-2xl font-bold text-gray-800 mb-4">Streak Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="text-center">
<div class="text-3xl font-bold text-green-600 mb-2">{stats.currentStreak}</div>
<div class="text-gray-600">Current Streak</div>
</div>
<div class="text-center">
<div class="text-3xl font-bold text-purple-600 mb-2">{stats.bestStreak}</div>
<div class="text-gray-600">Best Streak</div>
</div>
</div>
</div>
</Container>
<!-- Recent Performance -->
{#if stats.recentCompletions.length > 0}
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-2xl font-bold text-gray-800 mb-4">Recent Performance</h2>
<div class="space-y-3">
{#each stats.recentCompletions as completion}
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
<Container class="p-5 md:p-6">
<h2 class="text-lg md:text-xl font-bold text-gray-100 mb-4">Recent Performance</h2>
<div class="space-y-2">
{#each stats.recentCompletions as completion (completion.date)}
<div class="flex justify-between items-center py-2 border-b border-white/10 last:border-b-0">
<div>
<span class="font-medium">{formatDate(completion.date)}</span>
<span class="text-sm md:text-base font-medium text-gray-200">{formatDate(completion.date)}</span>
</div>
<div class="flex items-center gap-3">
<span class="text-gray-600">{completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}</span>
<span class="px-2 py-1 rounded text-sm font-semibold {getGradeColor(completion.grade)}">
<div class="flex items-center gap-2 md:gap-3">
<span class="text-xs md:text-sm text-gray-300">{completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}</span>
<span class="px-2 py-0.5 md:py-1 rounded text-xs md:text-sm font-semibold {getGradeColor(completion.grade)}">
{completion.grade}
</span>
</div>
</div>
{/each}
</div>
</div>
</Container>
{/if}
{/if}
{/if}
</div>
</div>
<AuthModal bind:isOpen={authModalOpen} anonymousId={""} />
<AuthModal bind:isOpen={authModalOpen} anonymousId="" />