Compare commits

...

4 Commits

Author SHA1 Message Date
George Powell
290fb06fe9 Reordered guesses table and added emphasis 2026-02-11 23:42:50 -05:00
George Powell
df8a9e62bb Add staggered page load animations
Implement elegant fadeInUp animations with staggered delays for main page
elements to create a polished, progressive reveal effect on page load.

Changes:
- layout.css: Added fadeInUp keyframes and delay utility classes
  (200ms, 400ms, 600ms, 800ms)
- +page.svelte: Applied animations to title, date, verse display,
  search input, guesses table, and credits

Animation sequence:
1. Title (0ms)
2. Date + Verse Display (200ms)
3. Search Input (400ms)
4. Guesses Table (600ms)
5. Credits (800ms - when won)

Creates a smooth, professional page load experience without changing any
existing design or functionality.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 17:24:14 -05:00
George Powell
730b65201a 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>
2026-02-11 13:01:53 -05:00
George Powell
78440cfbc3 Fix infinite stats submission and improve mobile button layout
- Fix infinite loop in stats submission effect by adding statsData to early return condition
- Make bottom buttons (View Stats, Sign In/Out) full-width on small screens
- Buttons now stack vertically on mobile, side-by-side on medium+ screens

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 21:05:10 -05:00
7 changed files with 326 additions and 138 deletions

View File

@@ -10,7 +10,7 @@
</script>
<div
class="inline-flex flex-col items-center bg-white/50 backdrop-blur-sm rounded-2xl border border-white/50 shadow-sm {className}"
class="inline-flex flex-col items-center bg-white/10 backdrop-blur-sm rounded-2xl border border-white/20 shadow-sm {className}"
>
{@render children()}
</div>

View File

@@ -99,47 +99,35 @@
class="flex gap-2 justify-center mb-4 pb-2 border-b border-gray-400"
>
<div
class="w-1/4 shrink-0 text-center text-sm font-semibold text-gray-700"
>
Book
</div>
<div
class="w-1/4 shrink-0 text-center text-sm font-semibold text-gray-700"
class="w-1/4 shrink-0 text-center text-sm font-bold text-gray-700"
>
Testament
</div>
<div
class="w-1/4 shrink-0 text-center text-sm font-semibold text-gray-700"
class="w-1/4 shrink-0 text-center text-sm font-bold text-gray-700"
>
Section
</div>
<div
class="w-1/4 shrink-0 text-center text-sm font-semibold text-gray-700"
class="w-1/4 shrink-0 text-center text-sm font-bold text-gray-700"
>
First Letter
</div>
<div
class="w-1/4 shrink-0 text-center text-sm font-bold text-gray-700"
>
Book
</div>
</div>
{#each guesses as guess, rowIndex (guess.book.id)}
<div class="flex gap-2 justify-center">
<!-- Book Column -->
<div
class="w-1/4 shrink-0 h-16 sm:h-20 md:h-24 border-2 border-opacity-80 rounded-lg flex items-center justify-center text-white font-bold text-base sm:text-lg md:text-xl shadow-lg animate-flip-in {getBoxColor(
guess.book.id === correctBookId,
)}"
style="animation-delay: {rowIndex * 1000 + 0 * 500}ms"
>
<span class="text-center leading-tight px-1 text-shadow-lg"
>{getBoxContent(guess, "book")}</span
>
</div>
<!-- Testament Column -->
<div
class="w-1/4 shrink-0 h-16 sm:h-20 md:h-24 border-2 border-opacity-80 rounded-lg flex items-center justify-center text-white font-bold text-base sm:text-lg md:text-xl shadow-md animate-flip-in {getBoxColor(
guess.testamentMatch,
)}"
style="animation-delay: {rowIndex * 1000 + 1 * 500}ms"
style="animation-delay: {rowIndex * 1000 + 0 * 500}ms"
>
<span class="text-center leading-tight px-1 text-shadow-sm"
>{getBoxContent(guess, "testament")}</span
@@ -152,7 +140,7 @@
guess.sectionMatch,
guess.adjacent,
)}"
style="animation-delay: {rowIndex * 1000 + 2 * 500}ms"
style="animation-delay: {rowIndex * 1000 + 1 * 500}ms"
>
<span class="text-center leading-tight px-1 text-shadow-sm"
>{getBoxContent(guess, "section")}
@@ -167,12 +155,24 @@
class="w-1/4 shrink-0 h-16 sm:h-20 md:h-24 border-2 border-opacity-80 rounded-lg flex items-center justify-center text-white font-bold text-base sm:text-lg md:text-xl shadow-md animate-flip-in {getBoxColor(
guess.firstLetterMatch,
)}"
style="animation-delay: {rowIndex * 1000 + 3 * 500}ms"
style="animation-delay: {rowIndex * 1000 + 2 * 500}ms"
>
<span class="text-center leading-tight px-1 text-shadow-sm"
>{getBoxContent(guess, "firstLetter")}</span
>
</div>
<!-- Book Column -->
<div
class="w-1/4 shrink-0 h-16 sm:h-20 md:h-24 border-4 border-opacity-100 rounded-lg flex items-center justify-center text-white font-bold text-base sm:text-lg md:text-xl shadow-lg animate-flip-in {getBoxColor(
guess.book.id === correctBookId,
)}"
style="animation-delay: {rowIndex * 1000 + 3 * 500}ms"
>
<span class="text-center leading-tight px-1 text-shadow-lg"
>{getBoxContent(guess, "book")}</span
>
</div>
</div>
{/each}
</div>

View File

@@ -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 {

View File

@@ -267,7 +267,7 @@
statsData,
});
if (!browser || !isWon || !anonymousId) {
if (!browser || !isWon || !anonymousId || statsData) {
console.log("Basic conditions not met");
return;
}
@@ -456,22 +456,26 @@
<div class="min-h-dvh md:bg-linear-to-br md:from-blue-50 md:to-indigo-200 py-8">
<div class="w-full max-w-3xl mx-auto px-4">
<h1
class="text-3xl md:text-4xl font-bold text-center uppercase text-gray-600 drop-shadow-2xl tracking-widest p-4"
class="text-3xl md:text-4xl font-bold text-center uppercase text-gray-600 drop-shadow-2xl tracking-widest p-4 animate-fade-in-up"
>
<TitleAnimation />
<div class="font-normal"></div>
</h1>
<div class="text-center mb-8">
<div class="text-center mb-8 animate-fade-in-up animate-delay-200">
<span class="big-text"
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
>
</div>
<div class="flex flex-col gap-6">
<VerseDisplay {data} {isWon} {blurChapter} />
<div class="animate-fade-in-up animate-delay-200">
<VerseDisplay {data} {isWon} {blurChapter} />
</div>
{#if !isWon}
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
<div class="animate-fade-in-up animate-delay-400">
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
</div>
{:else}
<WinScreen
{grade}
@@ -501,26 +505,30 @@
/>
{/if}
<GuessesTable {guesses} {correctBookId} />
<div class="animate-fade-in-up animate-delay-600">
<GuessesTable {guesses} {correctBookId} />
</div>
{#if isWon}
<Credits />
<div class="animate-fade-in-up animate-delay-800">
<Credits />
</div>
{/if}
</div>
<div class="mt-8 flex flex-col items-center gap-3">
<div class="flex gap-3">
<a
<div class="mt-8 flex flex-col items-stretch md:items-center gap-3">
<div class="flex flex-col md:flex-row gap-3">
<a
href="/stats?{user ? `userId=${user.id}` : `anonymousId=${anonymousId}`}"
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"
class="inline-flex items-center justify-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium shadow-md"
>
📊 View Stats
</a>
{#if user}
<form method="POST" action="/auth/logout" use:enhance>
<form method="POST" action="/auth/logout" use:enhance class="w-full md:w-auto">
<button
type="submit"
class="inline-flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium shadow-md"
class="inline-flex items-center justify-center w-full px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium shadow-md"
>
🚪 Sign Out
</button>
@@ -528,7 +536,7 @@
{:else}
<button
onclick={() => authModalOpen = true}
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium shadow-md"
class="inline-flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium shadow-md"
>
🔐 Sign In
</button>

View File

@@ -16,4 +16,36 @@ html, body {
letter-spacing: 0.2em;
color: rgb(107 114 128);
font-weight: 700;
}
/* Page load animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out both;
}
.animate-delay-200 {
animation-delay: 0.2s;
}
.animate-delay-400 {
animation-delay: 0.4s;
}
.animate-delay-600 {
animation-delay: 0.6s;
}
.animate-delay-800 {
animation-delay: 0.8s;
}

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

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="" />