diff --git a/.claude/settings.json b/.claude/settings.json index 480c385..ee58420 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,7 +5,6 @@ "Read(./secrets/**)", "Read(./config/credentials.json)", "Read(./build)", - "Read(./**.xml)", "Read(./embeddings**)" ] } diff --git a/.env.example b/.env.example index 7468339..a81adb0 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,19 @@ DATABASE_URL=example.db +PUBLIC_SITE_URL=https://bibdle.com + +# nodemailer +SMTP_USERNAME=email@example.com +SMTP_TOKEN=TOKEN +SMTP_SERVER=smtp.example.com +SMTP_PORT=port +# note from mail provider: Enable TLS or SSL on the external service if it is supported. + +# sign in with Discord + +# sign in with google + +# sign in with apple AUTH_SECRET=your-random-secret-here APPLE_ID=com.yourcompany.yourapp.client APPLE_TEAM_ID=your-team-id diff --git a/.gitignore b/.gitignore index c6b584a..6415f92 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ vite.config.ts.timestamp-* llms-* embeddings* -*.xml \ No newline at end of file +*bible.xml +engwebu_usfx.xml diff --git a/EnglishNKJBible.xml b/EnglishNKJBible.xml index 472ce20..90b784b 100644 --- a/EnglishNKJBible.xml +++ b/EnglishNKJBible.xml @@ -24143,7 +24143,7 @@ Gather the people, Sanctify the congregation, Assemble the elders, Gather the children and nursing babes; Let the bridegroom go out from his chamber, And the bride from her dressing room. Let the priests, who minister to the Lord, Weep between the porch and the altar; Let them say, “Spare Your people, O Lord, And do not give Your heritage to reproach, That the nations should rule over them. Why should they say among the peoples, ‘Where is their God?’ ” Then the Lord will be zealous for His land, And pity His people. - The Lord will answer and say to His people, “Behold, I will send you grain and new wine and oil, And you will be satisfied by them; I will no longer make you a reproach among the nations. + The Lord will answer and say to His people, “Behold, I will send you grain and new wine and oil, And you will be satisfied by them; I will no longer make you a reproach among the nations.” “But I will remove far from you the northern army, And will drive him away into a barren and desolate land, With his face toward the eastern sea And his back toward the western sea; His stench will come up, And his foul odor will rise, Because he has done monstrous things.” Fear not, O land; Be glad and rejoice, For the Lord has done marvelous things! Do not be afraid, you beasts of the field; For the open pastures are springing up, And the tree bears its fruit; The fig tree and the vine yield their strength. @@ -33616,4 +33616,4 @@ - \ No newline at end of file + diff --git a/analyze_top_users.sh b/analyze_top_users.sh new file mode 100755 index 0000000..1f33b81 --- /dev/null +++ b/analyze_top_users.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# analyze_top_users.sh +# Analyzes the daily_completions table to find the top 10 anonymous IDs by completion count + +# Set database path from argument or default to dev.db +DB_PATH="${1:-dev.db}" + +# Check if database file exists +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database file not found: $DB_PATH" + echo "Usage: $0 [database_path]" + exit 1 +fi + +# Run the analysis query +sqlite3 "$DB_PATH" < - import { onMount } from "svelte"; + import { onMount } from "svelte"; - interface ImposterData { - verses: string[]; - refs: string[]; - imposterIndex: number; - } + interface ImposterData { + verses: string[]; + refs: string[]; + imposterIndex: number; + } - let data: ImposterData | null = null; - let clicked: boolean[] = []; - let gameOver = false; - let loading = true; - let error: string | null = null; + let data: ImposterData | null = null; + let clicked: boolean[] = []; + let gameOver = false; + let loading = true; + let error: string | null = null; - async function loadGame() { - try { - const res = await fetch("/api/imposter"); - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - data = (await res.json()) as ImposterData; - clicked = new Array(data.verses.length).fill(false); - gameOver = false; - } catch (e) { - error = e instanceof Error ? e.message : "Unknown error"; - } finally { - loading = false; - } - } + async function loadGame() { + try { + const res = await fetch("/api/imposter"); + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + data = (await res.json()) as ImposterData; + clicked = new Array(data.verses.length).fill(false); + gameOver = false; + } catch (e) { + error = e instanceof Error ? e.message : "Unknown error"; + } finally { + loading = false; + } + } - function handleClick(index: number) { - if (gameOver || !data || clicked[index]) return; - clicked[index] = true; - if (index !== data.imposterIndex) { - clicked[data.imposterIndex] = true; - } - gameOver = true; - } + function handleClick(index: number) { + if (gameOver || !data || clicked[index]) return; + clicked[index] = true; + if (index !== data.imposterIndex) { + clicked[data.imposterIndex] = true; + } + gameOver = true; + } - function newGame() { - loading = true; - error = null; - data = null; - loadGame(); - } + function newGame() { + loading = true; + error = null; + data = null; + loadGame(); + } - onMount(loadGame); + onMount(loadGame); - function formatVerse(verse: string): string { - let formatted = verse; + function formatVerse(verse: string): string { + let formatted = verse; - // Handle unbalanced opening/closing punctuation - const pairs: [string, string][] = [ - ["(", ")"], - ["[", "]"], - ["{", "}"], - ['"', '"'], - ["'", "'"], - ["\u201C", "\u201D"], // \u201C - ["\u2018", "\u2019"], // \u2018 - ]; - for (const [open, close] of pairs) { - if (formatted.startsWith(open) && !formatted.includes(close)) { - formatted += "..." + close; - break; - } - } - for (const [open, close] of pairs) { - if (formatted.endsWith(close) && !formatted.includes(open)) { - formatted = open + "..." + formatted; - break; - } - } + // Handle unbalanced opening/closing punctuation + const pairs: [string, string][] = [ + ["(", ")"], + ["[", "]"], + ["{", "}"], + ['"', '"'], + ["'", "'"], + ["\u201C", "\u201D"], // \u201C + ["\u2018", "\u2019"], // \u2018 + ]; + for (const [open, close] of pairs) { + if (formatted.startsWith(open) && !formatted.includes(close)) { + formatted += "..." + close; + break; + } + } + for (const [open, close] of pairs) { + if (formatted.endsWith(close) && !formatted.includes(open)) { + formatted = open + "..." + formatted; + break; + } + } - if (/^[a-z]/.test(formatted)) { - formatted = "..." + formatted; - } - formatted = formatted.replace(/[,:;-—]$/, "..."); - return formatted; - } + if (/^[a-z]/.test(formatted)) { + formatted = "..." + formatted; + } + // Replace trailing punctuation with ellipsis + // Preserve closing quotes/brackets that may have been added + formatted = formatted.replace( + /[,:;-—]([)\]}\"\'\u201D\u2019]*)$/, + "...$1", + ); + return formatted; + }
- {#if loading} -

Loading verses...

- {:else if error} -
-

Error: {error}

- -
- {:else if data} - -
- {#each data.verses as verse, i} -
- - {#if gameOver} -
{data.refs[i]}
- {/if} -
- {/each} -
- {#if gameOver} -
- -
- {/if} - {/if} +
+ {#each data.verses as verse, i} +
+ + {#if gameOver} +
{data.refs[i]}
+ {/if} +
+ {/each} +
+ {#if gameOver} +
+ +
+ {/if} + {/if}
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 5e141fd..15eebeb 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,9 +1,8 @@ import { integer, sqliteTable, text, index, unique } from 'drizzle-orm/sqlite-core'; - import { sql } from 'drizzle-orm'; -export const user = sqliteTable('user', { - id: text('id').primaryKey(), +export const user = sqliteTable('user', { + id: text('id').primaryKey(), firstName: text('first_name'), lastName: text('last_name'), email: text('email').unique(), diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4108a20..e5f72a6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -21,11 +21,6 @@ - + {@render children()} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 2254638..238b383 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -93,13 +93,20 @@ export const actions: Actions = { const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length; const guessRank = betterGuesses + 1; + // Count ties: how many have the SAME guessCount (excluding self) + const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length; + // Average guesses const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0); const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10; + // Percentile: what percentage of people you beat (100 - your rank percentage) + const betterOrEqualCount = allCompletions.filter(c => c.guessCount <= guessCount).length; + const percentile = Math.round((betterOrEqualCount / totalSolves) * 100); + return { success: true, - stats: { solveRank, guessRank, totalSolves, averageGuesses } + stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount, percentile } }; } }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1ee0c80..494ed87 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -87,6 +87,11 @@ return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1); } + function getFirstLetter(bookName: string): string { + const match = bookName.match(/[a-zA-Z]/); + return match ? match[0] : bookName[0]; + } + function submitGuess(bookId: string) { if (guesses.some((g) => g.book.id === bookId)) return; @@ -103,15 +108,19 @@ // Special case: if correct book is in the Epistles + starts with "1", // any guess starting with "1" counts as first letter match const correctIsEpistlesWithNumber = - correctBook.section === "Pauline Epistles" && + (correctBook.section === "Pauline Epistles" || + correctBook.section === "General Epistles") && correctBook.name[0] === "1"; - const guessStartsWithNumber = book.name[0] === "1"; + const guessIsEpistlesWithNumber = + (book.section === "Pauline Epistles" || + book.section === "General Epistles") && + book.name[0] === "1"; const firstLetterMatch = - correctIsEpistlesWithNumber && guessStartsWithNumber + correctIsEpistlesWithNumber && guessIsEpistlesWithNumber ? true - : book.name[0].toUpperCase() === - correctBook.name[0].toUpperCase(); + : getFirstLetter(book.name).toUpperCase() === + getFirstLetter(correctBook.name).toUpperCase(); console.log( `Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`, @@ -215,15 +224,19 @@ // Apply same first letter logic as in submitGuess const correctIsEpistlesWithNumber = - correctBook.section === "Pauline Epistles" && + (correctBook.section === "Pauline Epistles" || + correctBook.section === "General Epistles") && correctBook.name[0] === "1"; - const guessStartsWithNumber = book.name[0] === "1"; + const guessIsEpistlesWithNumber = + (book.section === "Pauline Epistles" || + book.section === "General Epistles") && + book.name[0] === "1"; const firstLetterMatch = - correctIsEpistlesWithNumber && guessStartsWithNumber + correctIsEpistlesWithNumber && guessIsEpistlesWithNumber ? true - : book.name[0].toUpperCase() === - correctBook.name[0].toUpperCase(); + : getFirstLetter(book.name).toUpperCase() === + getFirstLetter(correctBook.name).toUpperCase(); return { book, @@ -433,7 +446,6 @@ - A daily bible game{isDev ? " (dev)" : ""}