mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
added some nice animation details
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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,7 +659,7 @@
|
|||||||
</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
|
||||||
@@ -618,13 +668,34 @@
|
|||||||
</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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user