Files
bibdle/src/routes/stats/+page.svelte
2026-02-28 02:48:46 -05:00

341 lines
9.0 KiB
Svelte

<script lang="ts">
import { browser } from "$app/environment";
import { onMount } from "svelte";
import AuthModal from "$lib/components/AuthModal.svelte";
import Container from "$lib/components/Container.svelte";
import { bibleBooks } from "$lib/types/bible";
import {
formatDate,
type UserStats,
} from "$lib/utils/stats";
import { fetchStreak } from "$lib/utils/streak";
interface PageData {
stats: UserStats | null;
error?: string;
user?: any;
session?: any;
requiresAuth?: boolean;
}
let { data }: { data: PageData } = $props();
let authModalOpen = $state(false);
let anonymousId = $state("");
let loading = $state(true);
let currentStreak = $state(0);
function getOrCreateAnonymousId(): string {
if (!browser) return "";
const key = "bibdle-anonymous-id";
let id = localStorage.getItem(key);
if (!id) {
id = crypto.randomUUID();
localStorage.setItem(key, id);
}
return id;
}
onMount(async () => {
anonymousId = getOrCreateAnonymousId();
if (data.user?.id) {
const localDate = new Date().toLocaleDateString("en-CA");
currentStreak = await fetchStreak(data.user.id, localDate);
} else {
currentStreak = data.stats?.currentStreak ?? 0;
}
loading = false;
});
function getBookName(bookId: string): string {
return bibleBooks.find((b) => b.id === bookId)?.name || bookId;
}
$inspect(data);
</script>
<svelte:head>
<title>Stats | Bibdle</title>
<meta
name="description"
content="View your Bibdle game statistics and performance"
/>
</svelte:head>
<div
class="min-h-screen bg-linear-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-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-300">Loading your stats...</p>
</div>
{:else if data.requiresAuth}
<div class="text-center py-12">
<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)}
class="inline-flex items-center justify-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
🔐 Sign In / Sign Up
</button>
<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>
</div>
</div>
</div>
{:else if data.error}
<div class="text-center py-12">
<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
</a>
</div>
</div>
{:else if !data.stats}
<div class="text-center py-12">
<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>
</Container>
</div>
{:else}
{@const stats = data.stats}
<!-- 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-2xl md:text-3xl mb-1">🔥</div>
<div
class="text-2xl md:text-3xl font-bold text-orange-400 mb-1"
>
{currentStreak}
</div>
<div
class="text-xs md:text-sm text-gray-300 font-medium"
>
Current Streak
</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 -->
<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-blue-400 mb-1"
>
{stats.avgGuesses}
</div>
<div
class="text-xs md:text-sm text-gray-300 font-medium"
>
Avg Guesses
</div>
</div>
</Container>
<!-- Total Solves -->
<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-green-400 mb-1"
>
{stats.totalSolves}
</div>
<div
class="text-xs md:text-sm text-gray-300 font-medium"
>
Total Solves
</div>
</div>
</Container>
</div>
{#if stats.totalSolves > 0}
<!-- 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>
{/if}
{/if}
</div>
</div>
<AuthModal bind:isOpen={authModalOpen} {anonymousId} />