Implement client-side timezone handling for daily verses

Refactored the daily verse system to properly handle users across different
timezones. Previously, the server used a fixed timezone (America/New_York),
causing users in other timezones to see incorrect verses near midnight.

Key changes:

**Server-side refactoring:**
- Extract `getVerseForDate()` into `src/lib/server/daily-verse.ts` for reuse
- Page load now uses UTC date for initial SSR (fast initial render)
- New `/api/daily-verse` POST endpoint accepts client-calculated date
- Server no longer calculates dates; uses client-provided date directly

**Client-side timezone handling:**
- Client calculates local date using browser's timezone on mount
- If server date doesn't match local date, fetches correct verse via API
- Changed verse data from `$derived` to `$state` to fix reactivity issues
- Mutating props was causing updates to fail; now uses local state
- Added effect to reload page when user returns to stale tab on new day

**Stats page improvements:**
- Accept `tz` query parameter for accurate streak calculations
- Use client's local date when determining "today" for current streaks
- Prevents timezone-based streak miscalculations

**Developer experience:**
- Added debug panel showing client local time vs daily verse date
- Added console logging for timezone fetch process
- Comprehensive test suite for timezone handling and streak logic

**UI improvements:**
- Share text uses 📜 emoji for logged-in users, 📖 for anonymous
- Stats link now includes timezone parameter for accurate display

This ensures users worldwide see the correct daily verse for their local
date, and streaks are calculated based on their timezone, not server time.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
George Powell
2026-02-12 23:37:08 -05:00
parent f6652e59a7
commit 77ffd6fbee
6 changed files with 653 additions and 50 deletions

View File

@@ -1,41 +1,17 @@
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { dailyVerses, dailyCompletions } from '$lib/server/db/schema';
import { eq, sql, asc } from 'drizzle-orm';
import { dailyCompletions } from '$lib/server/db/schema';
import { eq, asc } from 'drizzle-orm';
import { fail } from '@sveltejs/kit';
import { fetchRandomVerse } from '$lib/server/bible-api';
import { getBookById } from '$lib/server/bible';
import type { DailyVerse } from '$lib/server/db/schema';
import { getVerseForDate } from '$lib/server/daily-verse';
import crypto from 'node:crypto';
async function getTodayVerse(): Promise<DailyVerse> {
// Get the current date (server-side)
const dateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });
// If there's an existing verse for the current date, return it
const existing = await db.select().from(dailyVerses).where(eq(dailyVerses.date, dateStr)).limit(1);
if (existing.length > 0) {
return existing[0];
}
// Otherwise get a new random verse
const apiVerse = await fetchRandomVerse();
const createdAt = sql`${Math.floor(Date.now() / 1000)}`;
const newVerse: Omit<DailyVerse, 'createdAt'> = {
id: crypto.randomUUID(),
date: dateStr,
bookId: apiVerse.bookId,
verseText: apiVerse.verseText,
reference: apiVerse.reference,
};
const [inserted] = await db.insert(dailyVerses).values({ ...newVerse, createdAt }).returning();
return inserted;
}
export const load: PageServerLoad = async ({ locals }) => {
const dailyVerse = await getTodayVerse();
// Use UTC date for initial SSR; client will fetch timezone-correct verse if needed
const dateStr = new Date().toISOString().split('T')[0];
const dailyVerse = await getVerseForDate(dateStr);
const correctBook = getBookById(dailyVerse.bookId) ?? null;
return {