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">
import { browser } from "$app/environment";
import { fade } from "svelte/transition";
import type { PageData } from "../../routes/$types.js"; // Approximate type; adjust if needed
import Container from "./Container.svelte";
@@ -20,19 +22,52 @@
.replace(/^([a-z])/, (c) => c.toUpperCase())
.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>
<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
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
>
{displayVerseText}
</blockquote>
{#if isWon}
<p
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
class="transition-all duration-500 ease-in-out overflow-hidden"
style="max-height: {showReference ? '200px' : '0px'};"
>
{#if showReference}
<p
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>

View File

@@ -13,7 +13,7 @@
import DevButtons from "$lib/components/DevButtons.svelte";
import AuthModal from "$lib/components/AuthModal.svelte";
import { getGrade } from "$lib/utils/game";
import { enhance } from '$app/forms';
import { enhance } from "$app/forms";
interface Guess {
book: BibleBook;
@@ -64,6 +64,7 @@
);
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
let showWinScreen = $state(false);
let grade = $derived(
isWon
? guesses.length === 1 && chapterCorrect
@@ -183,18 +184,25 @@
$effect(() => {
if (!browser) return;
const localDate = new Date().toLocaleDateString('en-CA');
console.log('Date check:', { localDate, verseDate: dailyVerse.date, match: dailyVerse.date === localDate });
const localDate = new Date().toLocaleDateString("en-CA");
console.log("Date check:", {
localDate,
verseDate: dailyVerse.date,
match: dailyVerse.date === localDate,
});
if (dailyVerse.date === localDate) return;
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', {
method: 'POST',
fetch("/api/daily-verse", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({
date: localDate,
@@ -203,30 +211,36 @@
})
.then((res) => res.json())
.then((result) => {
console.log('Received verse data:', result);
console.log("Received verse data:", result);
dailyVerse = result.dailyVerse;
correctBookId = result.correctBookId;
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
$effect(() => {
if (!browser) return;
const loadedDate = new Date().toLocaleDateString('en-CA');
const loadedDate = new Date().toLocaleDateString("en-CA");
function onVisibilityChange() {
if (document.hidden) return;
const now = new Date().toLocaleDateString('en-CA');
const now = new Date().toLocaleDateString("en-CA");
if (now !== loadedDate) {
window.location.reload();
}
}
document.addEventListener('visibilitychange', onVisibilityChange);
return () => document.removeEventListener('visibilitychange', onVisibilityChange);
document.addEventListener("visibilitychange", onVisibilityChange);
return () =>
document.removeEventListener(
"visibilitychange",
onVisibilityChange,
);
});
// Initialize anonymous ID
@@ -366,7 +380,7 @@
async function submitStats() {
try {
const payload = {
anonymousId: anonymousId, // Already set correctly in $effect above
anonymousId: anonymousId, // Already set correctly in $effect above
date: dailyVerse.date,
guessCount: guesses.length,
};
@@ -405,6 +419,33 @@
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(() => {
if (!browser || !isWon) return;
const key = `bibdle-win-tracked-${dailyVerse.date}`;
@@ -455,7 +496,7 @@
lines.push(
`${emojis}${guesses.length === 1 && chapterCorrect ? " ⭐" : ""}`,
siteUrl
siteUrl,
);
return lines.join("\n");
@@ -548,7 +589,7 @@
<div class="animate-fade-in-up animate-delay-400">
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
</div>
{:else}
{:else if showWinScreen}
<div class="animate-fade-in-up animate-delay-400">
<WinScreen
{grade}
@@ -592,14 +633,23 @@
<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}`}&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"
>
📊 View Stats
</a>
{#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
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"
@@ -609,22 +659,43 @@
</form>
{:else}
<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"
>
🔐 Sign In
</button>
{/if}
</div>
{#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>User: {user ? `${user.email} (ID: ${user.id})` : 'Not signed in'}</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>
User: {user
? `${user.email} (ID: ${user.id})`
: "Not signed in"}
</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>
<DevButtons />