fixed weird signin bug

This commit is contained in:
George Powell
2026-02-12 20:24:38 -05:00
parent 290fb06fe9
commit f6652e59a7
5 changed files with 335 additions and 119 deletions

View File

@@ -0,0 +1,75 @@
import Database from 'bun:sqlite';
// Database path - adjust if your database is located elsewhere
const dbPath = Bun.env.DATABASE_URL || './local.db';
console.log(`Connecting to database: ${dbPath}`);
const db = new Database(dbPath);
interface DuplicateGroup {
anonymous_id: string;
date: string;
count: number;
}
interface Completion {
id: string;
anonymous_id: string;
date: string;
guess_count: number;
completed_at: number;
}
console.log('Finding duplicates...\n');
// Find all (anonymous_id, date) pairs with duplicates
const duplicatesQuery = db.query<DuplicateGroup, []>(`
SELECT anonymous_id, date, COUNT(*) as count
FROM daily_completions
GROUP BY anonymous_id, date
HAVING count > 1
`);
const duplicates = duplicatesQuery.all();
console.log(`Found ${duplicates.length} duplicate groups\n`);
if (duplicates.length === 0) {
console.log('No duplicates to clean up!');
db.close();
process.exit(0);
}
let totalDeleted = 0;
// Process each duplicate group
for (const dup of duplicates) {
// Get all completions for this (anonymous_id, date) pair
const completionsQuery = db.query<Completion, [string, string]>(`
SELECT id, anonymous_id, date, guess_count, completed_at
FROM daily_completions
WHERE anonymous_id = ? AND date = ?
ORDER BY completed_at ASC
`);
const completions = completionsQuery.all(dup.anonymous_id, dup.date);
console.log(` ${dup.anonymous_id} on ${dup.date}: ${completions.length} entries`);
// Keep the first (earliest completion), delete the rest
const toKeep = completions[0];
const toDelete = completions.slice(1);
console.log(` Keeping: ${toKeep.id} (completed at ${new Date(toKeep.completed_at * 1000).toISOString()})`);
const deleteQuery = db.query('DELETE FROM daily_completions WHERE id = ?');
for (const comp of toDelete) {
console.log(` Deleting: ${comp.id} (completed at ${new Date(comp.completed_at * 1000).toISOString()})`);
deleteQuery.run(comp.id);
totalDeleted++;
}
}
console.log(`\n✅ Deduplication complete!`);
console.log(`Total records deleted: ${totalDeleted}`);
console.log(`Unique completions preserved: ${duplicates.length}`);
db.close();

View File

@@ -41,6 +41,8 @@ export const dailyCompletions = sqliteTable('daily_completions', {
anonymousIdDateIndex: index('anonymous_id_date_idx').on(table.anonymousId, table.date), anonymousIdDateIndex: index('anonymous_id_date_idx').on(table.anonymousId, table.date),
dateIndex: index('date_idx').on(table.date), dateIndex: index('date_idx').on(table.date),
dateGuessIndex: index('date_guess_idx').on(table.date, table.guessCount), dateGuessIndex: index('date_guess_idx').on(table.date, table.guessCount),
// Ensures schema matches the database migration and prevents duplicate submissions
uniqueAnonymousIdDate: unique('daily_completions_anonymous_id_date_unique').on(table.anonymousId, table.date),
})); }));
export type DailyCompletion = typeof dailyCompletions.$inferSelect; export type DailyCompletion = typeof dailyCompletions.$inferSelect;

View File

@@ -181,10 +181,17 @@
// Initialize anonymous ID // Initialize anonymous ID
$effect(() => { $effect(() => {
if (!browser) return; if (!browser) return;
anonymousId = getOrCreateAnonymousId();
// CRITICAL: If user is logged in, ALWAYS use their user ID
// Never use the localStorage anonymous ID for authenticated users
if (user) {
anonymousId = user.id;
} else {
anonymousId = getOrCreateAnonymousId();
}
if ((window as any).umami) { if ((window as any).umami) {
// Use user id if logged in, otherwise use anonymous id (window as any).umami.identify(anonymousId);
(window as any).umami.identify(user ? user.id : anonymousId);
} }
const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`; const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`;
statsSubmitted = localStorage.getItem(statsKey) === "true"; statsSubmitted = localStorage.getItem(statsKey) === "true";
@@ -278,7 +285,7 @@
(async () => { (async () => {
try { try {
const response = await fetch( const response = await fetch(
`/api/submit-completion?anonymousId=${user ? user.id : anonymousId}&date=${dailyVerse.date}`, `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`,
); );
const result = await response.json(); const result = await response.json();
console.log("Stats response:", result); console.log("Stats response:", result);
@@ -308,7 +315,7 @@
async function submitStats() { async function submitStats() {
try { try {
const payload = { const payload = {
anonymousId: user ? user.id : anonymousId, anonymousId: anonymousId, // Already set correctly in $effect above
date: dailyVerse.date, date: dailyVerse.date,
guessCount: guesses.length, guessCount: guesses.length,
}; };
@@ -477,32 +484,34 @@
<SearchInput bind:searchQuery {guessedIds} {submitGuess} /> <SearchInput bind:searchQuery {guessedIds} {submitGuess} />
</div> </div>
{:else} {:else}
<WinScreen <div class="animate-fade-in-up animate-delay-400">
{grade} <WinScreen
{statsData} {grade}
{correctBookId} {statsData}
{handleShare} {correctBookId}
{copyToClipboard} {handleShare}
bind:copied {copyToClipboard}
{statsSubmitted} bind:copied
guessCount={guesses.length} {statsSubmitted}
reference={dailyVerse.reference} guessCount={guesses.length}
onChapterGuessCompleted={() => { reference={dailyVerse.reference}
chapterGuessCompleted = true; onChapterGuessCompleted={() => {
const key = `bibdle-chapter-guess-${dailyVerse.reference}`; chapterGuessCompleted = true;
const saved = localStorage.getItem(key); const key = `bibdle-chapter-guess-${dailyVerse.reference}`;
if (saved) { const saved = localStorage.getItem(key);
const data = JSON.parse(saved); if (saved) {
const match = const data = JSON.parse(saved);
dailyVerse.reference.match(/\s(\d+):/); const match =
const correctChapter = match dailyVerse.reference.match(/\s(\d+):/);
? parseInt(match[1], 10) const correctChapter = match
: 1; ? parseInt(match[1], 10)
chapterCorrect = : 1;
data.selectedChapter === correctChapter; chapterCorrect =
} data.selectedChapter === correctChapter;
}} }
/> }}
/>
</div>
{/if} {/if}
<div class="animate-fade-in-up animate-delay-600"> <div class="animate-fade-in-up animate-delay-600">

View File

@@ -42,54 +42,42 @@ export const actions: Actions = {
// Migrate anonymous stats if different anonymous ID // Migrate anonymous stats if different anonymous ID
if (anonymousId && anonymousId !== user.id) { if (anonymousId && anonymousId !== user.id) {
try { try {
// Update all daily completions from the local anonymous ID to the user's ID // Get completions for both the anonymous ID and the user ID
await db const anonCompletions = await db
.update(dailyCompletions) .select()
.set({ anonymousId: user.id }) .from(dailyCompletions)
.where(eq(dailyCompletions.anonymousId, anonymousId)); .where(eq(dailyCompletions.anonymousId, anonymousId));
console.log(`Migrated stats from ${anonymousId} to ${user.id}`);
// Deduplicate any entries for the same date after migration const userCompletions = await db
const allUserCompletions = await db
.select() .select()
.from(dailyCompletions) .from(dailyCompletions)
.where(eq(dailyCompletions.anonymousId, user.id)); .where(eq(dailyCompletions.anonymousId, user.id));
// Group by date to find duplicates // Create a set of dates the user already has completions for
const dateGroups = new Map<string, typeof allUserCompletions>(); const userDates = new Set(userCompletions.map(c => c.date));
for (const completion of allUserCompletions) {
const date = completion.date;
if (!dateGroups.has(date)) {
dateGroups.set(date, []);
}
dateGroups.get(date)!.push(completion);
}
// Process dates with duplicates let migrated = 0;
const duplicateIds: string[] = []; let skipped = 0;
for (const [date, completions] of dateGroups) {
if (completions.length > 1) { // Migrate only non-conflicting completions
// Sort by completedAt timestamp (earliest first) for (const completion of anonCompletions) {
completions.sort((a, b) => a.completedAt.getTime() - b.completedAt.getTime()); if (!userDates.has(completion.date)) {
// No conflict - safe to migrate
// Keep the first (earliest), mark the rest for deletion await db
const toDelete = completions.slice(1); .update(dailyCompletions)
duplicateIds.push(...toDelete.map(c => c.id)); .set({ anonymousId: user.id })
.where(eq(dailyCompletions.id, completion.id));
console.log(`Found ${completions.length} duplicates for date ${date}, keeping earliest, deleting ${toDelete.length}`); migrated++;
} else {
// Conflict exists - delete the anonymous completion (keep user's existing one)
await db
.delete(dailyCompletions)
.where(eq(dailyCompletions.id, completion.id));
skipped++;
} }
} }
// Delete duplicate entries console.log(`Migration complete: ${migrated} moved, ${skipped} duplicates removed`);
if (duplicateIds.length > 0) {
await db
.delete(dailyCompletions)
.where(inArray(dailyCompletions.id, duplicateIds));
console.log(`Deleted ${duplicateIds.length} duplicate completion entries`);
}
} catch (error) { } catch (error) {
console.error('Error migrating anonymous stats:', error); console.error('Error migrating anonymous stats:', error);
// Don't fail the signin if stats migration fails // Don't fail the signin if stats migration fails

View File

@@ -2,7 +2,7 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
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 Container from "$lib/components/Container.svelte";
import { bibleBooks } from "$lib/types/bible"; import { bibleBooks } from "$lib/types/bible";
@@ -11,7 +11,7 @@
formatDate, formatDate,
getStreakMessage, getStreakMessage,
getPerformanceMessage, getPerformanceMessage,
type UserStats type UserStats,
} from "$lib/utils/stats"; } from "$lib/utils/stats";
interface PageData { interface PageData {
@@ -47,7 +47,7 @@
} }
function getBookName(bookId: string): string { function getBookName(bookId: string): string {
return bibleBooks.find(b => b.id === bookId)?.name || bookId; return bibleBooks.find((b) => b.id === bookId)?.name || bookId;
} }
$inspect(data); $inspect(data);
@@ -55,15 +55,24 @@
<svelte:head> <svelte:head>
<title>Stats | Bibdle</title> <title>Stats | Bibdle</title>
<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-gray-900 via-slate-900 to-gray-900 p-4 md:p-8"> <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"> <div class="max-w-6xl mx-auto">
<!-- Header --> <!-- Header -->
<div class="text-center mb-6 md:mb-8"> <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> <h1 class="text-3xl md:text-4xl font-bold text-gray-100 mb-2">
<p class="text-sm md:text-base text-gray-300 mb-4">Track your Bibdle performance over time</p> Your Stats
</h1>
<p class="text-sm md:text-base text-gray-300 mb-4">
Track your Bibdle performance over time
</p>
<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 text-sm font-medium shadow-md" 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"
@@ -74,17 +83,25 @@
{#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-300">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-950/50 border border-blue-800/50 rounded-lg p-8 max-w-md mx-auto backdrop-blur-sm"> <div
<h2 class="text-2xl font-bold text-blue-200 mb-4">Authentication Required</h2> class="bg-blue-950/50 border border-blue-800/50 rounded-lg p-8 max-w-md mx-auto backdrop-blur-sm"
<p class="text-blue-300 mb-6">You must be logged in to see your stats.</p> >
<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"> <div class="flex flex-col gap-3">
<button <button
onclick={() => authModalOpen = true} 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" 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 🔐 Sign In / Sign Up
@@ -100,7 +117,9 @@
</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-950/50 border border-red-800/50 rounded-lg p-6 max-w-md mx-auto backdrop-blur-sm"> <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> <p class="text-red-300">{data.error}</p>
<a <a
href="/" href="/"
@@ -113,8 +132,12 @@
{:else if !data.stats} {:else if !data.stats}
<div class="text-center py-12"> <div class="text-center py-12">
<Container class="p-8 max-w-md mx-auto"> <Container class="p-8 max-w-md mx-auto">
<div class="text-yellow-400 mb-4 text-lg">No stats available yet.</div> <div class="text-yellow-400 mb-4 text-lg">
<p class="text-gray-300 mb-6">Start playing to build your stats!</p> No stats available yet.
</div>
<p class="text-gray-300 mb-6">
Start playing to build your stats!
</p>
<a <a
href="/" 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" 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"
@@ -132,8 +155,16 @@
<Container class="p-4 md:p-6"> <Container class="p-4 md:p-6">
<div class="text-center"> <div class="text-center">
<div class="text-2xl md:text-3xl mb-1">🔥</div> <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
<div class="text-xs md:text-sm text-gray-300 font-medium">Current Streak</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> </Container>
@@ -141,8 +172,16 @@
<Container class="p-4 md:p-6"> <Container class="p-4 md:p-6">
<div class="text-center"> <div class="text-center">
<div class="text-2xl md:text-3xl mb-1"></div> <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
<div class="text-xs md:text-sm text-gray-300 font-medium">Best Streak</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> </Container>
@@ -150,8 +189,16 @@
<Container class="p-4 md:p-6"> <Container class="p-4 md:p-6">
<div class="text-center"> <div class="text-center">
<div class="text-2xl md:text-3xl mb-1">🎯</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
<div class="text-xs md:text-sm text-gray-300 font-medium">Avg Guesses</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> </Container>
@@ -159,24 +206,46 @@
<Container class="p-4 md:p-6"> <Container class="p-4 md:p-6">
<div class="text-center"> <div class="text-center">
<div class="text-2xl md:text-3xl mb-1"></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
<div class="text-xs md:text-sm text-gray-300 font-medium">Total Solves</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> </Container>
</div> </div>
{#if stats.totalSolves > 0} {#if stats.totalSolves > 0}
<!-- Book Stats Grid --> <!-- Book Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4 mb-6"> <div
class="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4 mb-6"
>
<!-- Worst Day --> <!-- Worst Day -->
{#if stats.worstDay} {#if stats.worstDay}
<Container class="p-4 md:p-6"> <Container class="p-4 md:p-6">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">😅</div> <div class="text-3xl md:text-4xl">😅</div>
<div class="flex-1 min-w-0"> <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
<div class="text-xl md:text-2xl font-bold text-red-400 truncate">{stats.worstDay.guessCount} guesses</div> class="text-sm md:text-base text-gray-300 font-medium mb-1"
<div class="text-xs md:text-sm text-gray-400">{formatDate(stats.worstDay.date)}</div> >
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> </Container>
@@ -188,9 +257,22 @@
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">🏆</div> <div class="text-3xl md:text-4xl">🏆</div>
<div class="flex-1 min-w-0"> <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
<div class="text-lg md:text-xl font-bold text-amber-400 truncate">{getBookName(stats.bestBook.bookId)}</div> class="text-sm md:text-base text-gray-300 font-medium mb-1"
<div class="text-xs md:text-sm text-gray-400">{stats.bestBook.avgGuesses} avg guesses ({stats.bestBook.count}x)</div> >
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> </Container>
@@ -202,9 +284,24 @@
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">📖</div> <div class="text-3xl md:text-4xl">📖</div>
<div class="flex-1 min-w-0"> <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
<div class="text-lg md:text-xl font-bold text-indigo-400 truncate">{getBookName(stats.mostSeenBook.bookId)}</div> class="text-sm md:text-base text-gray-300 font-medium mb-1"
<div class="text-xs md:text-sm text-gray-400">{stats.mostSeenBook.count} time{stats.mostSeenBook.count === 1 ? '' : 's'}</div> >
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>
</div> </div>
</Container> </Container>
@@ -215,11 +312,20 @@
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div class="text-3xl md:text-4xl">📚</div> <div class="text-3xl md:text-4xl">📚</div>
<div class="flex-1 min-w-0"> <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
<div class="text-xl md:text-2xl font-bold text-teal-400"> class="text-sm md:text-base text-gray-300 font-medium mb-1"
{stats.totalBooksSeenOT + stats.totalBooksSeenNT} >
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 class="text-xs md:text-sm text-gray-400">OT: {stats.totalBooksSeenOT} / NT: {stats.totalBooksSeenNT}</div>
</div> </div>
</div> </div>
</Container> </Container>
@@ -227,18 +333,33 @@
<!-- Grade Distribution --> <!-- Grade Distribution -->
<Container class="p-5 md:p-6 mb-6"> <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> <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"> <div class="grid grid-cols-4 md:grid-cols-8 gap-2 md:gap-3">
{#each Object.entries(stats.gradeDistribution) as [grade, count] (grade)} {#each Object.entries(stats.gradeDistribution) as [grade, count] (grade)}
{@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-2 md:px-3 py-1 rounded-full text-xs md: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-lg md:text-2xl font-bold text-gray-100">{count}</div> <div
<div class="text-xs text-gray-400">{percentage}%</div> class="text-lg md:text-2xl font-bold text-gray-100"
>
{count}
</div>
<div class="text-xs text-gray-400">
{percentage}%
</div>
</div> </div>
{/each} {/each}
</div> </div>
@@ -247,16 +368,37 @@
<!-- Recent Performance --> <!-- Recent Performance -->
{#if stats.recentCompletions.length > 0} {#if stats.recentCompletions.length > 0}
<Container class="p-5 md:p-6"> <Container class="p-5 md:p-6">
<h2 class="text-lg md:text-xl font-bold text-gray-100 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-2"> <div class="space-y-2">
{#each stats.recentCompletions as completion (completion.date)} {#each stats.recentCompletions as completion, idx (`${completion.date}-${idx}`)}
<div class="flex justify-between items-center py-2 border-b border-white/10 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="text-sm md:text-base font-medium text-gray-200">{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-2 md:gap-3"> <div
<span class="text-xs md:text-sm text-gray-300">{completion.guessCount} guess{completion.guessCount === 1 ? '' : 'es'}</span> class="flex items-center gap-2 md:gap-3"
<span class="px-2 py-0.5 md:py-1 rounded text-xs md:text-sm font-semibold {getGradeColor(completion.grade)}"> >
<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} {completion.grade}
</span> </span>
</div> </div>
@@ -270,4 +412,4 @@
</div> </div>
</div> </div>
<AuthModal bind:isOpen={authModalOpen} anonymousId="" /> <AuthModal bind:isOpen={authModalOpen} anonymousId="" />