diff --git a/src/lib/components/WinScreen.svelte b/src/lib/components/WinScreen.svelte index b580e40..0e69813 100644 --- a/src/lib/components/WinScreen.svelte +++ b/src/lib/components/WinScreen.svelte @@ -1,6 +1,7 @@
-

+ +

+ {congratulationsMessage} The verse is from + {bookName}.

- import { bibleBooks, type BibleBook } from "$lib/types/bible"; + import { bibleBooks, type BibleBook } from "$lib/types/bible"; - import type { PageProps } from "./$types"; - import { browser } from "$app/environment"; + import type { PageProps } from "./$types"; + import { browser } from "$app/environment"; - import VerseDisplay from "$lib/components/VerseDisplay.svelte"; - import SearchInput from "$lib/components/SearchInput.svelte"; - import GuessesTable from "$lib/components/GuessesTable.svelte"; - import CountdownTimer from "$lib/components/CountdownTimer.svelte"; - import WinScreen from "$lib/components/WinScreen.svelte"; - import Feedback from "$lib/components/Feedback.svelte"; - import TitleAnimation from "$lib/components/TitleAnimation.svelte"; - import { getGrade } from "$lib/utils/game"; + import VerseDisplay from "$lib/components/VerseDisplay.svelte"; + import SearchInput from "$lib/components/SearchInput.svelte"; + import GuessesTable from "$lib/components/GuessesTable.svelte"; + import CountdownTimer from "$lib/components/CountdownTimer.svelte"; + import WinScreen from "$lib/components/WinScreen.svelte"; + import Feedback from "$lib/components/Feedback.svelte"; + import TitleAnimation from "$lib/components/TitleAnimation.svelte"; + import { getGrade } from "$lib/utils/game"; - interface Guess { - book: BibleBook; - testamentMatch: boolean; - sectionMatch: boolean; - adjacent: boolean; - } + interface Guess { + book: BibleBook; + testamentMatch: boolean; + sectionMatch: boolean; + adjacent: boolean; + } - let { data }: PageProps = $props(); + let { data }: PageProps = $props(); - let dailyVerse = $derived(data.dailyVerse); - let correctBookId = $derived(data.correctBookId); + let dailyVerse = $derived(data.dailyVerse); + let correctBookId = $derived(data.correctBookId); - let guesses = $state([]); + let guesses = $state([]); - let searchQuery = $state(""); + let searchQuery = $state(""); - let copied = $state(false); - let isDev = $state(false); + let copied = $state(false); + let isDev = $state(false); - let anonymousId = $state(""); - let statsSubmitted = $state(false); - let statsData = $state<{ - solveRank: number; - guessRank: number; - totalSolves: number; - averageGuesses: number; - } | null>(null); + let anonymousId = $state(""); + let statsSubmitted = $state(false); + let statsData = $state<{ + solveRank: number; + guessRank: number; + totalSolves: number; + averageGuesses: number; + } | null>(null); - let guessedIds = $derived(new Set(guesses.map((g) => g.book.id))); + let guessedIds = $derived(new Set(guesses.map((g) => g.book.id))); - const currentDate = $derived( - new Date().toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }), - ); + const currentDate = $derived( + new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }) + ); - let isWon = $derived(guesses.some((g) => g.book.id === correctBookId)); - let grade = $derived( - isWon - ? getGrade( - guesses.length, - getBookById(correctBookId)?.popularity ?? 0, - ) - : "", - ); + let isWon = $derived(guesses.some((g) => g.book.id === correctBookId)); + let grade = $derived( + isWon + ? getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0) + : "" + ); - function getBookById(id: string): BibleBook | undefined { - return bibleBooks.find((b) => b.id === id); - } + function getBookById(id: string): BibleBook | undefined { + return bibleBooks.find((b) => b.id === id); + } - function isAdjacent(id1: string, id2: string): boolean { - const b1 = getBookById(id1); - const b2 = getBookById(id2); - return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1); - } + function isAdjacent(id1: string, id2: string): boolean { + const b1 = getBookById(id1); + const b2 = getBookById(id2); + return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1); + } - function submitGuess(bookId: string) { - if (guesses.some((g) => g.book.id === bookId)) return; + function submitGuess(bookId: string) { + if (guesses.some((g) => g.book.id === bookId)) return; - const book = getBookById(bookId); - if (!book) return; + const book = getBookById(bookId); + if (!book) return; - const correctBook = getBookById(correctBookId); - if (!correctBook) return; + const correctBook = getBookById(correctBookId); + if (!correctBook) return; - const testamentMatch = book.testament === correctBook.testament; - const sectionMatch = book.section === correctBook.section; - const adjacent = isAdjacent(book.id, correctBookId); + const testamentMatch = book.testament === correctBook.testament; + const sectionMatch = book.section === correctBook.section; + const adjacent = isAdjacent(book.id, correctBookId); - console.log( - `Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`, - ); + console.log( + `Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}` + ); - guesses = [ - { - book, - testamentMatch, - sectionMatch, - adjacent, - }, - ...guesses, - ]; + if (guesses.length === 0 && browser && (window as any).umami) { + (window as any).umami.track("first-guess"); + } - searchQuery = ""; - } + guesses = [ + { + book, + testamentMatch, + sectionMatch, + adjacent, + }, + ...guesses, + ]; - function generateUUID(): string { - // Try native randomUUID if available - if (typeof window.crypto.randomUUID === "function") { - return window.crypto.randomUUID(); - } + searchQuery = ""; + } - // Fallback UUID v4 generator for older browsers - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { - const r = - window.crypto.getRandomValues(new Uint8Array(1))[0] % 16 | 0; - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } + function generateUUID(): string { + // Try native randomUUID if available + if (typeof window.crypto.randomUUID === "function") { + return window.crypto.randomUUID(); + } - function getOrCreateAnonymousId(): string { - if (!browser) return ""; - const key = "bibdle-anonymous-id"; - let id = localStorage.getItem(key); - if (!id) { - id = generateUUID(); - localStorage.setItem(key, id); - } - return id; - } + // Fallback UUID v4 generator for older browsers + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = window.crypto.getRandomValues(new Uint8Array(1))[0] % 16 | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } - // Initialize anonymous ID - $effect(() => { - if (!browser) return; - anonymousId = getOrCreateAnonymousId(); - const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`; - statsSubmitted = localStorage.getItem(statsKey) === "true"; - }); + function getOrCreateAnonymousId(): string { + if (!browser) return ""; + const key = "bibdle-anonymous-id"; + let id = localStorage.getItem(key); + if (!id) { + id = generateUUID(); + localStorage.setItem(key, id); + } + return id; + } - $effect(() => { - if (!browser) return; - isDev = window.location.host === "localhost:5173"; - }); + // Initialize anonymous ID + $effect(() => { + if (!browser) return; + anonymousId = getOrCreateAnonymousId(); + const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`; + statsSubmitted = localStorage.getItem(statsKey) === "true"; + }); - // Load saved guesses - $effect(() => { - if (!browser) return; + $effect(() => { + if (!browser) return; + isDev = window.location.host === "localhost:5173"; + }); - const key = `bibdle-guesses-${dailyVerse.date}`; - const saved = localStorage.getItem(key); - if (saved) { - let savedIds: string[] = JSON.parse(saved); - savedIds = Array.from(new Set(savedIds)); - guesses = savedIds.map((bookId: string) => { - const book = getBookById(bookId)!; - const correctBook = getBookById(correctBookId)!; - const testamentMatch = book.testament === correctBook.testament; - const sectionMatch = book.section === correctBook.section; - const adjacent = isAdjacent(bookId, correctBookId); - return { - book, - testamentMatch, - sectionMatch, - adjacent, - }; - }); - } - }); + // Load saved guesses + $effect(() => { + if (!browser) return; - $effect(() => { - if (!browser) return; - localStorage.setItem( - `bibdle-guesses-${dailyVerse.date}`, - JSON.stringify(guesses.map((g) => g.book.id)), - ); - }); + const key = `bibdle-guesses-${dailyVerse.date}`; + const saved = localStorage.getItem(key); + if (saved) { + let savedIds: string[] = JSON.parse(saved); + savedIds = Array.from(new Set(savedIds)); + guesses = savedIds.map((bookId: string) => { + const book = getBookById(bookId)!; + const correctBook = getBookById(correctBookId)!; + const testamentMatch = book.testament === correctBook.testament; + const sectionMatch = book.section === correctBook.section; + const adjacent = isAdjacent(bookId, correctBookId); + return { + book, + testamentMatch, + sectionMatch, + adjacent, + }; + }); + } + }); - // Auto-submit stats when user wins - $effect(() => { - console.log("Stats effect triggered:", { - browser, - isWon, - anonymousId, - statsSubmitted, - statsData, - }); + $effect(() => { + if (!browser) return; + localStorage.setItem( + `bibdle-guesses-${dailyVerse.date}`, + JSON.stringify(guesses.map((g) => g.book.id)) + ); + }); - if (!browser || !isWon || !anonymousId) { - console.log("Basic conditions not met"); - return; - } + // Auto-submit stats when user wins + $effect(() => { + console.log("Stats effect triggered:", { + browser, + isWon, + anonymousId, + statsSubmitted, + statsData, + }); - if (statsSubmitted && !statsData) { - console.log("Fetching existing stats..."); + if (!browser || !isWon || !anonymousId) { + console.log("Basic conditions not met"); + return; + } - (async () => { - try { - const response = await fetch( - `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`, - ); - const result = await response.json(); - console.log("Stats response:", result); + if (statsSubmitted && !statsData) { + console.log("Fetching existing stats..."); - if (result.success && result.stats) { - console.log("Setting stats data:", result.stats); - statsData = result.stats; - localStorage.setItem( - `bibdle-stats-submitted-${dailyVerse.date}`, - "true", - ); - } else if (result.error) { - console.error("Server error:", result.error); - } else { - console.error("Unexpected response format:", result); - } - } catch (err) { - console.error("Stats fetch failed:", err); - } - })(); + (async () => { + try { + const response = await fetch( + `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}` + ); + const result = await response.json(); + console.log("Stats response:", result); - return; - } + if (result.success && result.stats) { + console.log("Setting stats data:", result.stats); + statsData = result.stats; + localStorage.setItem( + `bibdle-stats-submitted-${dailyVerse.date}`, + "true" + ); + } else if (result.error) { + console.error("Server error:", result.error); + } else { + console.error("Unexpected response format:", result); + } + } catch (err) { + console.error("Stats fetch failed:", err); + } + })(); - console.log("Submitting stats..."); + return; + } - async function submitStats() { - try { - const payload = { - anonymousId, - date: dailyVerse.date, - guessCount: guesses.length, - }; + console.log("Submitting stats..."); - console.log("Sending POST request with:", payload); + async function submitStats() { + try { + const payload = { + anonymousId, + date: dailyVerse.date, + guessCount: guesses.length, + }; - const response = await fetch("/api/submit-completion", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); + console.log("Sending POST request with:", payload); - const result = await response.json(); - console.log("Stats response:", result); + const response = await fetch("/api/submit-completion", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); - if (result.success && result.stats) { - console.log("Setting stats data:", result.stats); - statsData = result.stats; - statsSubmitted = true; - localStorage.setItem( - `bibdle-stats-submitted-${dailyVerse.date}`, - "true", - ); - } else if (result.error) { - console.error("Server error:", result.error); - } else { - console.error("Unexpected response format:", result); - } - } catch (err) { - console.error("Stats submission failed:", err); - } - } + const result = await response.json(); + console.log("Stats response:", result); - submitStats(); - }); + if (result.success && result.stats) { + console.log("Setting stats data:", result.stats); + statsData = result.stats; + statsSubmitted = true; + localStorage.setItem( + `bibdle-stats-submitted-${dailyVerse.date}`, + "true" + ); + } else if (result.error) { + console.error("Server error:", result.error); + } else { + console.error("Unexpected response format:", result); + } + } catch (err) { + console.error("Stats submission failed:", err); + } + } - function generateShareText(): string { - const emojis = guesses - .slice() - .reverse() - .map((guess) => { - if (guess.book.id === correctBookId) return "✅"; - if (guess.adjacent) return "‼️"; - if (guess.sectionMatch) return "🟩"; - if (guess.testamentMatch) return "🟧"; - return "🟥"; - }) - .join(""); + submitStats(); + }); - const dateFormatter = new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); - const formattedDate = dateFormatter.format( - new Date(`${dailyVerse.date}T00:00:00`), - ); - const siteUrl = window.location.origin; - return [ - `📖 Bibdle | ${formattedDate} 📖`, - `${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`, - `${emojis}`, - siteUrl, - ].join("\n"); - } + function generateShareText(): string { + const emojis = guesses + .slice() + .reverse() + .map((guess) => { + if (guess.book.id === correctBookId) return "✅"; + if (guess.adjacent) return "‼️"; + if (guess.sectionMatch) return "🟩"; + if (guess.testamentMatch) return "🟧"; + return "🟥"; + }) + .join(""); - async function share() { - if (!browser) return; + const dateFormatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + const formattedDate = dateFormatter.format( + new Date(`${dailyVerse.date}T00:00:00`) + ); + const siteUrl = window.location.origin; + return [ + `📖 Bibdle | ${formattedDate} 📖`, + `${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`, + `${emojis}`, + siteUrl, + ].join("\n"); + } - const shareText = generateShareText(); + async function share() { + if (!browser) return; - try { - if ("share" in navigator) { - await (navigator as any).share({ text: shareText }); - } else { - await (navigator as any).clipboard.writeText(shareText); - } - } catch (err) { - console.error("Share failed:", err); - throw err; - } - } + const shareText = generateShareText(); - async function copyToClipboard() { - if (!browser) return; + try { + if ("share" in navigator) { + await (navigator as any).share({ text: shareText }); + } else { + await (navigator as any).clipboard.writeText(shareText); + } + } catch (err) { + console.error("Share failed:", err); + throw err; + } + } - const shareText = generateShareText(); + async function copyToClipboard() { + if (!browser) return; - try { - await (navigator as any).clipboard.writeText(shareText); - copied = true; - setTimeout(() => { - copied = false; - }, 5000); - } catch (err) { - console.error("Copy to clipboard failed:", err); - throw err; - } - } + const shareText = generateShareText(); - function handleShare() { - if (copied || !browser) return; - const useClipboard = !("share" in navigator); - if (useClipboard) { - copied = true; - } - share() - .then(() => { - if (useClipboard) { - setTimeout(() => { - copied = false; - }, 5000); - } - }) - .catch(() => { - if (useClipboard) { - copied = false; - } - }); - } + try { + await (navigator as any).clipboard.writeText(shareText); + copied = true; + setTimeout(() => { + copied = false; + }, 5000); + } catch (err) { + console.error("Copy to clipboard failed:", err); + throw err; + } + } - function clearLocalStorage() { - if (!browser) return; - // Clear all bibdle-related localStorage items - const keysToRemove: string[] = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.startsWith("bibdle-")) { - keysToRemove.push(key); - } - } - keysToRemove.forEach((key) => localStorage.removeItem(key)); - // Reload the page to reset state - window.location.reload(); - } + function handleShare() { + if (copied || !browser) return; + const useClipboard = !("share" in navigator); + if (useClipboard) { + copied = true; + } + share() + .then(() => { + if (useClipboard) { + setTimeout(() => { + copied = false; + }, 5000); + } + }) + .catch(() => { + if (useClipboard) { + copied = false; + } + }); + } + + function clearLocalStorage() { + if (!browser) return; + // Clear all bibdle-related localStorage items + const keysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith("bibdle-")) { + keysToRemove.push(key); + } + } + keysToRemove.forEach((key) => localStorage.removeItem(key)); + // Reload the page to reset state + window.location.reload(); + } - Bibdle — A daily bible game{isDev ? " (dev)" : ""} - + Bibdle — A daily bible game{isDev ? " (dev)" : ""} +

-
-

- -
-

-
- {isDev ? "Dev Edition | " : ""}{currentDate} -
+
+

+ +
+

+
+ {isDev ? "Dev Edition | " : ""}{currentDate} +
- + - {#if !isWon} - - {:else} - - - {/if} + {#if !isWon} + + {:else} + + + {/if} - - {#if isWon} - - {/if} - {#if isDev} - - {/if} -
+ + {#if isWon} + + {/if} + {#if isDev} + + {/if} +
diff --git a/todo.md b/todo.md index 8e56d71..a02acba 100644 --- a/todo.md +++ b/todo.md @@ -1,43 +1,38 @@ # in progress - - root menu: classic / imposter mode / impossible mode (complete today's classic and imposter modes to unlock) - # todo - impossible mode (1904 greek bible) three guesses only. - - share both classic and impossible mode with both buttons + + - share both classic and impossible mode with both buttons - add imposter mode - instructions - - classic mode: identify what book the verse is from (e.g. Genesis, John, Revelations...) in as few guesses as possible. - - imposter mode: out of four options, identify the verse that is not in the Bible - - impossible mode: identify which book of the bible the verse is from in less than three guesses. + + - classic mode: identify what book the verse is from (e.g. Genesis, John, Revelations...) in as few guesses as possible. + - imposter mode: out of four options, identify the verse that is not in the Bible + - impossible mode: identify which book of the bible the verse is from in less than three guesses. - add login + saved stats + streak etc. - add deuterocanonical books + - Practice mode: Unlimited verses - Create public or private leaderboards -- Passport book with badges: +- Passport book with awards: + - Guess each Gospel first try - "Guessed all Gospels", "Perfect week", "Old Testament expert" - Theologian: Guess each book first try - - + - If chapter is 6 and verse 7, earn award "Six seven" - difficult mode (guess old or new testament, first try _only_) (???) -# places to send - -- linkedin post -- ocf discord server ✅ -- nick makiej ✅ - # About this game As a young camper at the Metropolis of Boston Camp, I remember His Eminence Metropolitan Methodios would visit every Sunday. He was often surrounded by important people for his entire time there, so I never gathered the courage to introduce myself, but his homilies during Liturgy always stood out to me. In some ways, they differed year after year, but a majority of his message remained strikingly familiar. "Take ten minutes to read the Bible every day," he asked. "Just ten minutes. Go somewhere quiet, turn off the TV (then iPod, then cell phone), and read in peace and quiet." @@ -52,6 +47,10 @@ I created Bibdle from a combination of two things. The first is my lifelong desi # done +## december 27th + +- add event log to submitting first-guess or correct-guess to umami (to make bounce rate more accurate) + ## december 26th - created embeddings for every bible verse (verse similarity finder)