mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
Compare commits
4 Commits
482ee0a83a
...
290fb06fe9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290fb06fe9 | ||
|
|
df8a9e62bb | ||
|
|
730b65201a | ||
|
|
78440cfbc3 |
@@ -10,7 +10,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,47 +99,35 @@
|
|||||||
class="flex gap-2 justify-center mb-4 pb-2 border-b border-gray-400"
|
class="flex gap-2 justify-center mb-4 pb-2 border-b border-gray-400"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
>
|
|
||||||
Book
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="w-1/4 shrink-0 text-center text-sm font-semibold text-gray-700"
|
|
||||||
>
|
>
|
||||||
Testament
|
Testament
|
||||||
</div>
|
</div>
|
||||||
<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
|
Section
|
||||||
</div>
|
</div>
|
||||||
<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
|
First Letter
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-1/4 shrink-0 text-center text-sm font-bold text-gray-700"
|
||||||
|
>
|
||||||
|
Book
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each guesses as guess, rowIndex (guess.book.id)}
|
{#each guesses as guess, rowIndex (guess.book.id)}
|
||||||
<div class="flex gap-2 justify-center">
|
<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 -->
|
<!-- Testament Column -->
|
||||||
<div
|
<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(
|
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,
|
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"
|
<span class="text-center leading-tight px-1 text-shadow-sm"
|
||||||
>{getBoxContent(guess, "testament")}</span
|
>{getBoxContent(guess, "testament")}</span
|
||||||
@@ -152,7 +140,7 @@
|
|||||||
guess.sectionMatch,
|
guess.sectionMatch,
|
||||||
guess.adjacent,
|
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"
|
<span class="text-center leading-tight px-1 text-shadow-sm"
|
||||||
>{getBoxContent(guess, "section")}
|
>{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(
|
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,
|
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"
|
<span class="text-center leading-tight px-1 text-shadow-sm"
|
||||||
>{getBoxContent(guess, "firstLetter")}</span
|
>{getBoxContent(guess, "firstLetter")}</span
|
||||||
>
|
>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,21 @@ export interface UserStats {
|
|||||||
guessCount: number;
|
guessCount: number;
|
||||||
grade: string;
|
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 {
|
export function getGradeColor(grade: string): string {
|
||||||
|
|||||||
@@ -267,7 +267,7 @@
|
|||||||
statsData,
|
statsData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!browser || !isWon || !anonymousId) {
|
if (!browser || !isWon || !anonymousId || statsData) {
|
||||||
console.log("Basic conditions not met");
|
console.log("Basic conditions not met");
|
||||||
return;
|
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="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">
|
<div class="w-full max-w-3xl mx-auto px-4">
|
||||||
<h1
|
<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 />
|
<TitleAnimation />
|
||||||
<div class="font-normal"></div>
|
<div class="font-normal"></div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8 animate-fade-in-up animate-delay-200">
|
||||||
<span class="big-text"
|
<span class="big-text"
|
||||||
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
|
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="animate-fade-in-up animate-delay-200">
|
||||||
<VerseDisplay {data} {isWon} {blurChapter} />
|
<VerseDisplay {data} {isWon} {blurChapter} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if !isWon}
|
{#if !isWon}
|
||||||
|
<div class="animate-fade-in-up animate-delay-400">
|
||||||
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
|
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<WinScreen
|
<WinScreen
|
||||||
{grade}
|
{grade}
|
||||||
@@ -501,26 +505,30 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="animate-fade-in-up animate-delay-600">
|
||||||
<GuessesTable {guesses} {correctBookId} />
|
<GuessesTable {guesses} {correctBookId} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if isWon}
|
{#if isWon}
|
||||||
|
<div class="animate-fade-in-up animate-delay-800">
|
||||||
<Credits />
|
<Credits />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-8 flex flex-col items-center gap-3">
|
<div class="mt-8 flex flex-col items-stretch md:items-center gap-3">
|
||||||
<div class="flex gap-3">
|
<div class="flex flex-col md:flex-row gap-3">
|
||||||
<a
|
<a
|
||||||
href="/stats?{user ? `userId=${user.id}` : `anonymousId=${anonymousId}`}"
|
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
|
📊 View Stats
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if user}
|
{#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
|
<button
|
||||||
type="submit"
|
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
|
🚪 Sign Out
|
||||||
</button>
|
</button>
|
||||||
@@ -528,7 +536,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
onclick={() => authModalOpen = true}
|
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
|
🔐 Sign In
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,3 +17,35 @@ html, body {
|
|||||||
color: rgb(107 114 128);
|
color: rgb(107 114 128);
|
||||||
font-weight: 700;
|
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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { db } from '$lib/server/db';
|
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 { eq, desc } from 'drizzle-orm';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { bibleBooks } from '$lib/types/bible';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ url, locals }) => {
|
export const load: PageServerLoad = async ({ url, locals }) => {
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
@@ -51,7 +52,12 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
|||||||
},
|
},
|
||||||
currentStreak: 0,
|
currentStreak: 0,
|
||||||
bestStreak: 0,
|
bestStreak: 0,
|
||||||
recentCompletions: []
|
recentCompletions: [],
|
||||||
|
worstDay: null,
|
||||||
|
bestBook: null,
|
||||||
|
mostSeenBook: null,
|
||||||
|
totalBooksSeenOT: 0,
|
||||||
|
totalBooksSeenNT: 0
|
||||||
},
|
},
|
||||||
user: locals.user,
|
user: locals.user,
|
||||||
session: locals.session
|
session: locals.session
|
||||||
@@ -133,6 +139,66 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
|||||||
grade: getGradeFromGuesses(c.guessCount)
|
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 {
|
return {
|
||||||
stats: {
|
stats: {
|
||||||
totalSolves,
|
totalSolves,
|
||||||
@@ -140,7 +206,22 @@ export const load: PageServerLoad = async ({ url, locals }) => {
|
|||||||
gradeDistribution,
|
gradeDistribution,
|
||||||
currentStreak,
|
currentStreak,
|
||||||
bestStreak,
|
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,
|
user: locals.user,
|
||||||
session: locals.session
|
session: locals.session
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import AuthModal from "$lib/components/AuthModal.svelte";
|
import AuthModal from "$lib/components/AuthModal.svelte";
|
||||||
|
import Container from "$lib/components/Container.svelte";
|
||||||
|
import { bibleBooks } from "$lib/types/bible";
|
||||||
import {
|
import {
|
||||||
getGradeColor,
|
getGradeColor,
|
||||||
formatDate,
|
formatDate,
|
||||||
@@ -44,6 +46,10 @@
|
|||||||
return total > 0 ? Math.round((count / total) * 100) : 0;
|
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);
|
$inspect(data);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -52,32 +58,30 @@
|
|||||||
<meta name="description" content="View your Bibdle game statistics and performance" />
|
<meta name="description" content="View your Bibdle game statistics and performance" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen bg-gradient-to-br from-amber-50 to-orange-100 p-4">
|
<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-4xl mx-auto">
|
<div class="max-w-6xl mx-auto">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-6 md:mb-8">
|
||||||
<h1 class="text-4xl font-bold text-gray-800 mb-2">Your Stats</h1>
|
<h1 class="text-3xl md:text-4xl font-bold text-gray-100 mb-2">Your Stats</h1>
|
||||||
<p class="text-gray-600">Track your Bibdle performance over time</p>
|
<p class="text-sm md:text-base text-gray-300 mb-4">Track your Bibdle performance over time</p>
|
||||||
<div class="mt-4">
|
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="inline-flex items-center px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
|
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
|
← Back to Game
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="text-center py-12">
|
<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>
|
<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>
|
</div>
|
||||||
{:else if data.requiresAuth}
|
{:else if data.requiresAuth}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="bg-blue-100 border border-blue-300 rounded-lg p-8 max-w-md mx-auto">
|
<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-800 mb-4">Authentication Required</h2>
|
<h2 class="text-2xl font-bold text-blue-200 mb-4">Authentication Required</h2>
|
||||||
<p class="text-blue-700 mb-6">You must be logged in to see your stats.</p>
|
<p class="text-blue-300 mb-6">You must be logged in to see your stats.</p>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<button
|
<button
|
||||||
onclick={() => authModalOpen = true}
|
onclick={() => authModalOpen = true}
|
||||||
@@ -87,7 +91,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href="/"
|
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"
|
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
|
← Back to Game
|
||||||
</a>
|
</a>
|
||||||
@@ -96,8 +100,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if data.error}
|
{:else if data.error}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="bg-red-100 border border-red-300 rounded-lg p-6 max-w-md mx-auto">
|
<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-700">{data.error}</p>
|
<p class="text-red-300">{data.error}</p>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="mt-4 inline-block px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
class="mt-4 inline-block px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||||
@@ -108,114 +112,162 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if !data.stats}
|
{:else if !data.stats}
|
||||||
<div class="text-center py-12">
|
<div class="text-center py-12">
|
||||||
<div class="bg-yellow-100 border border-yellow-300 rounded-lg p-6 max-w-md mx-auto">
|
<Container class="p-8 max-w-md mx-auto">
|
||||||
<p class="text-yellow-700">No stats available.</p>
|
<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
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="mt-4 inline-block px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 transition-colors"
|
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
|
Start Playing
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{@const stats = data.stats}
|
{@const stats = data.stats}
|
||||||
|
|
||||||
<!-- Overview Cards -->
|
<!-- Key Stats Grid -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-6">
|
||||||
<!-- Total Solves -->
|
<!-- Current Streak -->
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<Container class="p-4 md:p-6">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-3xl font-bold text-amber-600 mb-2">{stats.totalSolves}</div>
|
<div class="text-2xl md:text-3xl mb-1">🔥</div>
|
||||||
<div class="text-gray-600">Total Solves</div>
|
<div class="text-2xl md:text-3xl font-bold text-orange-400 mb-1">{stats.currentStreak}</div>
|
||||||
{#if stats.totalSolves > 0}
|
<div class="text-xs md:text-sm text-gray-300 font-medium">Current Streak</div>
|
||||||
<div class="text-sm text-gray-500 mt-1">
|
|
||||||
{getPerformanceMessage(stats.avgGuesses)}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</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>
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<!-- Average Guesses -->
|
<!-- 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-center">
|
||||||
<div class="text-3xl font-bold text-blue-600 mb-2">{stats.avgGuesses}</div>
|
<div class="text-2xl md:text-3xl mb-1">🎯</div>
|
||||||
<div class="text-gray-600">Avg. Guesses</div>
|
<div class="text-2xl md:text-3xl font-bold text-blue-400 mb-1">{stats.avgGuesses}</div>
|
||||||
<div class="text-sm text-gray-500 mt-1">per solve</div>
|
<div class="text-xs md:text-sm text-gray-300 font-medium">Avg Guesses</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Current Streak -->
|
{#if stats.totalSolves > 0}
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<!-- Book Stats Grid -->
|
||||||
<div class="text-center">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4 mb-6">
|
||||||
<div class="text-3xl font-bold text-green-600 mb-2">{stats.currentStreak}</div>
|
<!-- Worst Day -->
|
||||||
<div class="text-gray-600">Current Streak</div>
|
{#if stats.worstDay}
|
||||||
<div class="text-sm text-gray-500 mt-1">
|
<Container class="p-4 md:p-6">
|
||||||
{getStreakMessage(stats.currentStreak)}
|
<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>
|
||||||
</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>
|
||||||
</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 -->
|
<!-- Grade Distribution -->
|
||||||
{#if stats.totalSolves > 0}
|
<Container class="p-5 md:p-6 mb-6">
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
|
<h2 class="text-lg md:text-xl font-bold text-gray-100 mb-4">Grade Distribution</h2>
|
||||||
<h2 class="text-2xl font-bold text-gray-800 mb-4">Grade Distribution</h2>
|
<div class="grid grid-cols-4 md:grid-cols-8 gap-2 md:gap-3">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
{#each Object.entries(stats.gradeDistribution) as [grade, count] (grade)}
|
||||||
{#each Object.entries(stats.gradeDistribution) as [grade, count]}
|
|
||||||
{@const percentage = getGradePercentage(count, stats.totalSolves)}
|
{@const percentage = getGradePercentage(count, stats.totalSolves)}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="mb-2">
|
<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}
|
{grade}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-2xl font-bold text-gray-800">{count}</div>
|
<div class="text-lg md:text-2xl font-bold text-gray-100">{count}</div>
|
||||||
<div class="text-sm text-gray-500">{percentage}%</div>
|
<div class="text-xs text-gray-400">{percentage}%</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- Recent Performance -->
|
<!-- Recent Performance -->
|
||||||
{#if stats.recentCompletions.length > 0}
|
{#if stats.recentCompletions.length > 0}
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<Container class="p-5 md:p-6">
|
||||||
<h2 class="text-2xl font-bold text-gray-800 mb-4">Recent Performance</h2>
|
<h2 class="text-lg md:text-xl font-bold text-gray-100 mb-4">Recent Performance</h2>
|
||||||
<div class="space-y-3">
|
<div class="space-y-2">
|
||||||
{#each stats.recentCompletions as completion}
|
{#each stats.recentCompletions as completion (completion.date)}
|
||||||
<div class="flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0">
|
<div class="flex justify-between items-center py-2 border-b border-white/10 last:border-b-0">
|
||||||
<div>
|
<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>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-2 md:gap-3">
|
||||||
<span class="text-gray-600">{completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}</span>
|
<span class="text-xs md:text-sm text-gray-300">{completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}</span>
|
||||||
<span class="px-2 py-1 rounded text-sm font-semibold {getGradeColor(completion.grade)}">
|
<span class="px-2 py-0.5 md:py-1 rounded text-xs md:text-sm font-semibold {getGradeColor(completion.grade)}">
|
||||||
{completion.grade}
|
{completion.grade}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AuthModal bind:isOpen={authModalOpen} anonymousId={""} />
|
<AuthModal bind:isOpen={authModalOpen} anonymousId="" />
|
||||||
Reference in New Issue
Block a user