From 21efdd4eeff1adb48701f7c86614d67fe80c87d2 Mon Sep 17 00:00:00 2001 From: George Powell Date: Tue, 7 Jul 2026 17:02:24 -0400 Subject: [PATCH] New scheduling UI! --- spec.md | 8 +- src/hooks.server.ts | 4 +- src/lib/components/Button.svelte | 3 + src/lib/components/GamePrompt.svelte | 2 - src/lib/components/SubmitVerse.svelte | 1001 +++++++++++++++++ src/lib/components/VerseDisplay.svelte | 13 +- src/lib/components/WinScreen.svelte | 11 +- src/lib/server/auth.test.ts | 10 +- src/lib/server/auth.ts | 10 +- src/lib/server/bible-api.ts | 19 + src/lib/server/daily-verse.ts | 33 +- src/lib/server/verse-submission.ts | 293 +++++ src/routes/+page.svelte | 1 + src/routes/api/submit-verse/+server.ts | 89 ++ src/routes/api/submit-verse/status/+server.ts | 69 ++ src/routes/auth/apple/callback/+server.ts | 2 +- src/routes/auth/google/callback/+server.ts | 2 +- src/routes/scheduled-verses/+page.svelte | 83 +- tests/verse-submission.test.ts | 513 +++++++++ 19 files changed, 2127 insertions(+), 39 deletions(-) create mode 100644 src/lib/components/SubmitVerse.svelte create mode 100644 src/lib/server/verse-submission.ts create mode 100644 src/routes/api/submit-verse/+server.ts create mode 100644 src/routes/api/submit-verse/status/+server.ts create mode 100644 tests/verse-submission.test.ts diff --git a/spec.md b/spec.md index 3745953..5ed71ba 100644 --- a/spec.md +++ b/spec.md @@ -5,10 +5,10 @@ - [x] **Step 1 — Schema & migration:** add `verse_submissions` table to `src/lib/server/db/schema.ts` (nullable `user_id` FK → `user.id` ON DELETE SET NULL, unique `scheduled_date`, index on `user_id`), pushed to dev.db. - [x] **Step 2 — Bible structure + windowing helpers:** export `getChapterCount`/`getVerseCount` from `xml-bible.ts`; add `composeVerseWindow()` (fall-forward 3-verse window, never crosses book, crosses chapter within book) and `formatWindowReference()` (hyphen same-chapter, en-dash cross-chapter). Tests in `tests/verse-window.test.ts` (17 pass). - [x] **Step 3 — Public APIs:** `GET /api/bible/structure` (66-book verse counts for cascading dropdowns) and `GET /api/verse-window` (live 3-verse preview). -- [ ] **Step 4 — Admin module + `/scheduled-verses` page:** `src/lib/server/admin.ts` (`ADMIN_EMAIL`), auth-walled admin view joining `verse_submissions` → `daily_verses` → `user`. -- [ ] **Step 5 — Submit backend + lazy gap-fill:** `POST /api/submit-verse` (solved-today gate, 7-day cooldown, structural validation, scheduling scan with empty/no-back-to-back/60-day-repeat rules, transaction + retry), `GET /api/submit-verse/status`, modified `getVerseForDate` neighbor avoidance. -- [ ] **Step 6 — Frontend `SubmitVerse.svelte`:** three states (logged-out sign-in dropdown, cooldown + countdown, cascading selects + preview + submit), wired into `WinScreen.svelte`. -- [ ] **Step 7 — Tests:** scheduling validity, cooldown arithmetic, concurrency. +- [x] **Step 4 — Admin module + `/scheduled-verses` page:** `src/lib/server/admin.ts` (`ADMIN_EMAIL`), auth-walled admin view joining `verse_submissions` → `daily_verses` → `user`. +- [x] **Step 5 — Submit backend + lazy gap-fill:** `POST /api/submit-verse` (solved-today gate, 7-day cooldown, structural validation, scheduling scan with empty/no-back-to-back/60-day-repeat rules, transaction + retry), `GET /api/submit-verse/status`, modified `getVerseForDate` neighbor avoidance. +- [x] **Step 6 — Frontend `SubmitVerse.svelte`:** three states (logged-out sign-in dropdown, cooldown + countdown, cascading selects + preview + submit), wired into `WinScreen.svelte`. +- [x] **Step 7 — Tests:** scheduling validity, cooldown arithmetic, concurrency. - [ ] **Step 8 — Docs:** update README schema + routes/API tables. --- diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 5209e6e..66a24c7 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -17,9 +17,9 @@ const handleAuth: Handle = async ({ event, resolve }) => { const { session, user } = await auth.validateSessionToken(sessionToken); if (session) { - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); + auth.setSessionTokenCookie({ cookies: event.cookies }, sessionToken, session.expiresAt); } else { - auth.deleteSessionTokenCookie(event); + auth.deleteSessionTokenCookie({ cookies: event.cookies }); } event.locals.user = user; diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 9bb58aa..1e32aeb 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -7,6 +7,7 @@ onclick?: () => void; class?: string; type?: "button" | "submit" | "reset"; + disabled?: boolean; } let { @@ -15,6 +16,7 @@ onclick, class: className = "", type = "button", + disabled = false, }: Props = $props(); const variantClasses = { @@ -31,6 +33,7 @@ + + {:else if !isLoggedIn} + + + {#if expanded} +

+ Sign in to submit a verse for a future day +

+
+ + +
+
+ + +
+ {/if} + {:else if onCooldown} + + + {#if countdownText} +

Next submission in {countdownText}

+ {/if} + {:else} + + + {#if expanded} +
+ {#if statusError} +

{statusError}

+ {/if} + + {#if !structure} +

Loading Bible structure…

+ {:else} +
+ + + + + +
+ + +
+ {#if previewLoading} +

Loading preview…

+ {:else if preview} +

{preview.reference}

+
+ {#each preview.windowVerses as verseText, i (i)} +

{verseText}

+ {/each} +
+ {:else} +

Select a verse to preview.

+ {/if} +
+ + {#if submitError} +

{submitError}

+ {/if} + + + {#if status?.upcoming && status.upcoming.length > 0} +

+ Your upcoming verse{status.upcoming.length > 1 + ? "s" + : ""}: + {#each status.upcoming as u, i (i)} + {#if i > 0}·{/if} + {u.reference} ({fmtDateVague(u.scheduledDate)}) + {/each} +

+ {/if} + {/if} +
+ {/if} + {/if} + + + diff --git a/src/lib/components/VerseDisplay.svelte b/src/lib/components/VerseDisplay.svelte index bc81a69..a954f98 100644 --- a/src/lib/components/VerseDisplay.svelte +++ b/src/lib/components/VerseDisplay.svelte @@ -1,14 +1,21 @@