added some nice animation details

This commit is contained in:
George Powell
2026-02-13 00:36:06 -05:00
parent 77ffd6fbee
commit a12c7d011a
2 changed files with 140 additions and 34 deletions

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment";
import { fade } from "svelte/transition";
import type { PageData } from "../../routes/$types.js"; // Approximate type; adjust if needed import type { PageData } from "../../routes/$types.js"; // Approximate type; adjust if needed
import Container from "./Container.svelte"; import Container from "./Container.svelte";
@@ -20,19 +22,52 @@
.replace(/^([a-z])/, (c) => c.toUpperCase()) .replace(/^([a-z])/, (c) => c.toUpperCase())
.replace(/[,:;-—]$/, "...") .replace(/[,:;-—]$/, "...")
); );
let showReference = $state(false);
// Delay showing reference until GuessesTable animation completes
$effect(() => {
if (!isWon) {
showReference = false;
return;
}
// Check if user already won today (page reload case)
const winTrackedKey = `bibdle-win-tracked-${dailyVerse.date}`;
const alreadyWonToday = browser && localStorage.getItem(winTrackedKey) === "true";
if (alreadyWonToday) {
// User already won and is refreshing - show immediately
showReference = true;
} else {
// User just won this session - delay for animation
const animationDelay = 1800;
const timeoutId = setTimeout(() => {
showReference = true;
}, animationDelay);
return () => clearTimeout(timeoutId);
}
});
</script> </script>
<Container class="w-full p-8 sm:p-12 bg-white/70"> <Container class="w-full p-8 sm:p-12 bg-white/70 overflow-hidden">
<blockquote <blockquote
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center" class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
> >
{displayVerseText} {displayVerseText}
</blockquote> </blockquote>
{#if isWon} <div
<p class="transition-all duration-500 ease-in-out overflow-hidden"
class="text-center text-lg! big-text text-green-600! font-bold mt-8 bg-white/70 rounded-xl px-4 py-2" style="max-height: {showReference ? '200px' : '0px'};"
> >
{displayReference} {#if showReference}
</p> <p
{/if} transition:fade={{ duration: 400 }}
class="text-center text-lg! big-text text-green-600! font-bold mt-8 bg-white/70 rounded-xl px-4 py-2"
>
{displayReference}
</p>
{/if}
</div>
</Container> </Container>

View File

@@ -13,7 +13,7 @@
import DevButtons from "$lib/components/DevButtons.svelte"; import DevButtons from "$lib/components/DevButtons.svelte";
import AuthModal from "$lib/components/AuthModal.svelte"; import AuthModal from "$lib/components/AuthModal.svelte";
import { getGrade } from "$lib/utils/game"; import { getGrade } from "$lib/utils/game";
import { enhance } from '$app/forms'; import { enhance } from "$app/forms";
interface Guess { interface Guess {
book: BibleBook; book: BibleBook;
@@ -64,6 +64,7 @@
); );
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId)); let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
let showWinScreen = $state(false);
let grade = $derived( let grade = $derived(
isWon isWon
? guesses.length === 1 && chapterCorrect ? guesses.length === 1 && chapterCorrect
@@ -183,18 +184,25 @@
$effect(() => { $effect(() => {
if (!browser) return; if (!browser) return;
const localDate = new Date().toLocaleDateString('en-CA'); const localDate = new Date().toLocaleDateString("en-CA");
console.log('Date check:', { localDate, verseDate: dailyVerse.date, match: dailyVerse.date === localDate }); console.log("Date check:", {
localDate,
verseDate: dailyVerse.date,
match: dailyVerse.date === localDate,
});
if (dailyVerse.date === localDate) return; if (dailyVerse.date === localDate) return;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log('Fetching timezone-correct verse:', { localDate, timezone }); console.log("Fetching timezone-correct verse:", {
localDate,
timezone,
});
fetch('/api/daily-verse', { fetch("/api/daily-verse", {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
date: localDate, date: localDate,
@@ -203,30 +211,36 @@
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((result) => { .then((result) => {
console.log('Received verse data:', result); console.log("Received verse data:", result);
dailyVerse = result.dailyVerse; dailyVerse = result.dailyVerse;
correctBookId = result.correctBookId; correctBookId = result.correctBookId;
correctBook = result.correctBook; correctBook = result.correctBook;
}) })
.catch((err) => console.error('Failed to fetch timezone-correct verse:', err)); .catch((err) =>
console.error("Failed to fetch timezone-correct verse:", err),
);
}); });
// Reload when the user returns to a stale tab on a new calendar day // Reload when the user returns to a stale tab on a new calendar day
$effect(() => { $effect(() => {
if (!browser) return; if (!browser) return;
const loadedDate = new Date().toLocaleDateString('en-CA'); const loadedDate = new Date().toLocaleDateString("en-CA");
function onVisibilityChange() { function onVisibilityChange() {
if (document.hidden) return; if (document.hidden) return;
const now = new Date().toLocaleDateString('en-CA'); const now = new Date().toLocaleDateString("en-CA");
if (now !== loadedDate) { if (now !== loadedDate) {
window.location.reload(); window.location.reload();
} }
} }
document.addEventListener('visibilitychange', onVisibilityChange); document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener('visibilitychange', onVisibilityChange); return () =>
document.removeEventListener(
"visibilitychange",
onVisibilityChange,
);
}); });
// Initialize anonymous ID // Initialize anonymous ID
@@ -366,7 +380,7 @@
async function submitStats() { async function submitStats() {
try { try {
const payload = { const payload = {
anonymousId: anonymousId, // Already set correctly in $effect above anonymousId: anonymousId, // Already set correctly in $effect above
date: dailyVerse.date, date: dailyVerse.date,
guessCount: guesses.length, guessCount: guesses.length,
}; };
@@ -405,6 +419,33 @@
submitStats(); submitStats();
}); });
// Delay showing win screen until GuessesTable animation completes
$effect(() => {
if (!isWon) {
showWinScreen = false;
return;
}
// Check if user already won today (page reload case)
const winTrackedKey = `bibdle-win-tracked-${dailyVerse.date}`;
const alreadyWonToday =
browser && localStorage.getItem(winTrackedKey) === "true";
if (alreadyWonToday) {
// User already won and is refreshing - show immediately
showWinScreen = true;
} else {
// User just won this session - delay for animation
// Animation timing: last column starts at 1500ms, animation takes 600ms
const animationDelay = 1800;
const timeoutId = setTimeout(() => {
showWinScreen = true;
}, animationDelay);
return () => clearTimeout(timeoutId);
}
});
$effect(() => { $effect(() => {
if (!browser || !isWon) return; if (!browser || !isWon) return;
const key = `bibdle-win-tracked-${dailyVerse.date}`; const key = `bibdle-win-tracked-${dailyVerse.date}`;
@@ -455,7 +496,7 @@
lines.push( lines.push(
`${emojis}${guesses.length === 1 && chapterCorrect ? " ⭐" : ""}`, `${emojis}${guesses.length === 1 && chapterCorrect ? " ⭐" : ""}`,
siteUrl siteUrl,
); );
return lines.join("\n"); return lines.join("\n");
@@ -548,7 +589,7 @@
<div class="animate-fade-in-up animate-delay-400"> <div class="animate-fade-in-up animate-delay-400">
<SearchInput bind:searchQuery {guessedIds} {submitGuess} /> <SearchInput bind:searchQuery {guessedIds} {submitGuess} />
</div> </div>
{:else} {:else if showWinScreen}
<div class="animate-fade-in-up animate-delay-400"> <div class="animate-fade-in-up animate-delay-400">
<WinScreen <WinScreen
{grade} {grade}
@@ -592,14 +633,23 @@
<div class="mt-8 flex flex-col items-stretch md:items-center gap-3"> <div class="mt-8 flex flex-col items-stretch md:items-center gap-3">
<div class="flex flex-col md:flex-row gap-3"> <div class="flex flex-col md:flex-row gap-3">
<a <a
href="/stats?{user ? `userId=${user.id}` : `anonymousId=${anonymousId}`}&tz={encodeURIComponent(Intl.DateTimeFormat().resolvedOptions().timeZone)}" href="/stats?{user
? `userId=${user.id}`
: `anonymousId=${anonymousId}`}&tz={encodeURIComponent(
Intl.DateTimeFormat().resolvedOptions().timeZone,
)}"
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" 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 class="w-full md:w-auto"> <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 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" 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"
@@ -609,22 +659,43 @@
</form> </form>
{:else} {:else}
<button <button
onclick={() => authModalOpen = true} onclick={() => (authModalOpen = true)}
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" 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>
{/if} {/if}
</div> </div>
{#if isDev} {#if isDev}
<div class="text-xs text-gray-600 bg-gray-100 px-3 py-2 rounded border"> <div
class="text-xs text-gray-600 bg-gray-100 px-3 py-2 rounded border"
>
<div><strong>Debug Info:</strong></div> <div><strong>Debug Info:</strong></div>
<div>User: {user ? `${user.email} (ID: ${user.id})` : 'Not signed in'}</div> <div>
<div>Session: {session ? `Expires ${session.expiresAt.toLocaleDateString()}` : 'No session'}</div> User: {user
<div>Anonymous ID: {anonymousId || 'Not set'}</div> ? `${user.email} (ID: ${user.id})`
<div>Client Local Time: {new Date().toLocaleString('en-US', { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, timeZoneName: 'short' })}</div> : "Not signed in"}
<div>Client Local Date: {new Date().toLocaleDateString('en-CA')}</div> </div>
<div>
Session: {session
? `Expires ${session.expiresAt.toLocaleDateString()}`
: "No session"}
</div>
<div>Anonymous ID: {anonymousId || "Not set"}</div>
<div>
Client Local Time: {new Date().toLocaleString("en-US", {
timeZone:
Intl.DateTimeFormat().resolvedOptions()
.timeZone,
timeZoneName: "short",
})}
</div>
<div>
Client Local Date: {new Date().toLocaleDateString(
"en-CA",
)}
</div>
<div>Daily Verse Date: {dailyVerse.date}</div> <div>Daily Verse Date: {dailyVerse.date}</div>
</div> </div>
<DevButtons /> <DevButtons />