switched to NKJV, improved grading, improved styling

This commit is contained in:
George Powell
2025-12-23 17:33:33 -05:00
parent 93acafc232
commit f9f0928278
16 changed files with 34345 additions and 68 deletions

1
.rooignore Normal file
View File

@@ -0,0 +1 @@
EnglishNKJBible.xml

33619
EnglishNKJBible.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
"name": "bibdle",
"dependencies": {
"better-sqlite3": "^12.5.0",
"fast-xml-parser": "^5.3.3",
},
"devDependencies": {
"@oslojs/crypto": "^1.0.1",
@@ -274,6 +275,8 @@
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"fast-xml-parser": ["fast-xml-parser@5.3.3", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
@@ -406,6 +409,8 @@
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svelte": ["svelte@5.46.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="],

View File

@@ -34,6 +34,7 @@
"vite": "^7.2.6"
},
"dependencies": {
"better-sqlite3": "^12.5.0"
"better-sqlite3": "^12.5.0",
"fast-xml-parser": "^5.3.3"
}
}

View File

@@ -0,0 +1,302 @@
# Local Bible XML Implementation Plan
## Overview
Replace the external Bible API ([`fetchRandomVerse()`](../src/lib/server/bible-api.ts:6)) with a local XML-based solution using the downloaded NKJV Bible XML file.
## Current Implementation Analysis
### Current Flow
The system currently:
1. Calls [`fetchRandomVerse()`](../src/lib/server/bible-api.ts:6) which hits `https://bible-api.com`
2. Gets a random verse position (book, chapter, verse)
3. Fetches 3 consecutive verses starting from that position
4. Returns `{ bookId, reference, verseText }` as [`ApiVerse`](../src/lib/server/bible-api.ts:4)
5. Stores this in the database via [`getTodayVerse()`](../src/routes/+page.server.ts:11)
### Key Data Structures
- **Book IDs**: 3-letter codes (e.g., 'GEN', 'MAT', '1CO') defined in [`bibleBooks`](../src/lib/types/bible.ts:24)
- **Book Order**: Sequential 1-66 via the `order` property
- **Return Format**: `{ bookId: string, reference: string, verseText: string }`
### XML Structure
```xml
<bible translation="English NKJ 1982">
<testament name="Old">
<book number="1">
<chapter number="1">
<verse number="1">Text here</verse>
```
Books are numbered 1-66, matching the `order` property in [`bibleBooks`](../src/lib/types/bible.ts:24).
## Solution Architecture
### Book Number Mapping Strategy
Since the XML uses numbers (1-66) and the system uses book IDs ('GEN', 'MAT', etc.), we'll create a mapping using the existing [`bibleBooks`](../src/lib/types/bible.ts:24) array:
```typescript
// book number -> book ID
const bookNumberToId = {
1: "GEN", // Genesis
2: "EXO", // Exodus
// ... through ...
40: "MAT", // Matthew (first NT book)
// ... to ...
66: "REV", // Revelation
};
```
This can be generated programmatically from the existing [`bibleBooks`](../src/lib/types/bible.ts:24) array using the `order` property.
### XML Parsing Approach
We'll use a streaming/lazy approach to avoid loading the entire XML into memory:
1. **Random Selection**: Pick random book number (1-66), chapter, and verse
2. **Targeted Parsing**: Parse only the needed section of the XML
3. **Verse Extraction**: Extract 3 consecutive verses from that position
### Implementation Flow
```mermaid
graph TD
A[getTodayVerse called] --> B[fetchRandomVerse]
B --> C[Generate random position]
C --> D[Load XML file]
D --> E[Parse specific book/chapter]
E --> F[Extract 3 consecutive verses]
F --> G[Map book number to ID]
G --> H[Format reference string]
H --> I[Return ApiVerse object]
I --> J[Store in database]
```
## Technical Implementation Plan
### Step 1: Create Book Mapping Utility
**File**: [`src/lib/server/bible.ts`](../src/lib/server/bible.ts)
Add a new mapping object and helper function:
- Create `bookNumberToId` map derived from [`bibleBooks`](../src/lib/types/bible.ts:24)
- Create `getBookByNumber(number: number): BibleBook | undefined`
- Create reverse map `bookIdToNumber` for potential future use
### Step 2: Install XML Parser
**Command**: Install a lightweight XML parser
Options:
- `fast-xml-parser` - Fast, lightweight, good for selective parsing
- `xml2js` - Popular, stable
- Node's built-in stream parser for maximum efficiency
Recommendation: `fast-xml-parser` for balance of speed and ease of use.
### Step 3: Create XML Utility Module
**File**: `src/lib/server/xml-bible.ts` (new file)
Functions needed:
- `loadBibleXml()`: Read the XML file from disk
- `parseBook(xml: string, bookNumber: number)`: Extract specific book data
- `getChapterVerseCount(bookNumber: number, chapterNumber: number)`: Get verse count for validation
- `extractVerses(bookNumber: number, chapter: number, startVerse: number, count: number)`: Get consecutive verses
### Step 4: Implement Random Verse Selection Logic
**File**: `src/lib/server/xml-bible.ts`
Algorithm:
1. Pick random book number (1-66)
2. Parse that book to get chapter count
3. Pick random chapter
4. Parse that chapter to get verse count
5. Pick random starting verse (ensuring room for 3 verses)
6. Extract verses and verse count
**Optimization**: Consider pre-computing chapter/verse counts to avoid parsing overhead.
### Step 5: Create New fetchRandomVerse Implementation
**File**: [`src/lib/server/bible-api.ts`](../src/lib/server/bible-api.ts)
Replace the current implementation:
- Remove external API calls
- Use new XML parsing functions
- Maintain the same return type [`ApiVerse`](../src/lib/server/bible-api.ts:4)
- Format reference string (e.g., "Matthew 1:1-3")
### Step 6: Handle Edge Cases
Important considerations:
- **End-of-chapter**: If starting verse is near end, may get fewer than 3 verses
- **End-of-book**: Don't cross chapter boundaries
- **Short chapters**: Some chapters have only 1-2 verses
- **Book names**: Generate proper reference strings (e.g., "1 Samuel" not "1SA")
### Step 7: Testing Strategy
1. **Manual testing**: Run the app and verify verses load correctly
2. **Verify book mapping**: Ensure all 66 books map correctly
3. **Check reference format**: Ensure references match expected format
4. **Edge case testing**: Test with short chapters/books
5. **Performance**: Ensure parsing is fast enough for production
## Data Structure Details
### Book Number to ID Mapping
```typescript
// Complete mapping based on existing bibleBooks array
const BOOK_MAPPING = {
// Old Testament (1-39)
1: "GEN",
2: "EXO",
3: "LEV",
4: "NUM",
5: "DEU",
6: "JOS",
7: "JDG",
8: "RUT",
9: "1SA",
10: "2SA",
11: "1KI",
12: "2KI",
13: "1CH",
14: "2CH",
15: "EZR",
16: "NEH",
17: "EST",
18: "JOB",
19: "PSA",
20: "PRO",
21: "ECC",
22: "SNG",
23: "ISA",
24: "JER",
25: "LAM",
26: "EZK",
27: "DAN",
28: "HOS",
29: "JOL",
30: "AMO",
31: "OBA",
32: "JON",
33: "MIC",
34: "NAM",
35: "HAB",
36: "ZEP",
37: "HAG",
38: "ZEC",
39: "MAL",
// New Testament (40-66)
40: "MAT",
41: "MRK",
42: "LUK",
43: "JHN",
44: "ACT",
45: "ROM",
46: "1CO",
47: "2CO",
48: "GAL",
49: "EPH",
50: "PHP",
51: "COL",
52: "1TH",
53: "2TH",
54: "1TI",
55: "2TI",
56: "TIT",
57: "PHM",
58: "HEB",
59: "JAS",
60: "1PE",
61: "2PE",
62: "1JN",
63: "2JN",
64: "3JN",
65: "JUD",
66: "REV",
};
```
### Reference String Format
The reference should match the current format from the API:
- Format: `"{Book Name} {Chapter}:{StartVerse}-{EndVerse}"`
- Example: `"Matthew 1:1-3"` or `"1 Corinthians 13:4-6"`
Use the `name` property from the [`BibleBook`](../src/lib/types/bible.ts:14) interface.
## File Changes Summary
### New Files
- `src/lib/server/xml-bible.ts` - XML parsing and verse extraction logic
### Modified Files
- [`src/lib/server/bible.ts`](../src/lib/server/bible.ts) - Add book number mapping utilities
- [`src/lib/server/bible-api.ts`](../src/lib/server/bible-api.ts) - Replace API calls with local XML parsing
- `package.json` - Add XML parser dependency
### No Changes Required
- [`src/routes/+page.server.ts`](../src/routes/+page.server.ts) - Uses [`fetchRandomVerse()`](../src/lib/server/bible-api.ts:6), internal implementation change is transparent
- [`src/lib/types/bible.ts`](../src/lib/types/bible.ts) - Existing types remain the same
- Database schema - No changes needed
## Performance Considerations
### Memory Usage
- Avoid loading entire XML file into memory
- Parse only the needed book/chapter sections
- Use streaming or selective parsing
### Speed Optimization Options
1. **Lazy parsing**: Only parse when needed (slower, low memory)
2. **Pre-parsed index**: Create JSON index of book/chapter/verse counts (faster, more memory)
3. **Caching**: Cache parsed books in memory during runtime (fastest, higher memory)
Recommendation: Start with lazy parsing, optimize if needed.
### File Size
The XML file is large but will be read from disk once per verse generation. Since verses are cached per day in the database, this happens once daily.
## Rollback Strategy
If issues arise:
1. Keep the old [`fetchRandomVerse()`](../src/lib/server/bible-api.ts:6) implementation as `fetchRandomVerseAPI()`
2. Add environment variable to toggle between implementations
3. Can quickly revert by changing which function is exported
## Next Steps After Implementation
1. Monitor daily verse generation for errors
2. Verify reference strings are formatted correctly
3. Consider adding verse length validation (avoid very short/long passages)
4. Potentially add weighted random selection to favor popular books
## Questions to Resolve
1. Should we validate that selected verses have minimum text length?
2. Should we add any filtering to avoid certain books or chapters?
3. Do we want to pre-compute chapter/verse counts for faster selection?

View File

@@ -82,6 +82,12 @@
animation: shine 5s infinite;
}
.animate-shine.fade-in {
animation:
fadeIn 0.5s ease-out,
shine 5s infinite;
}
@keyframes fadeIn {
from {
opacity: 0;

View File

@@ -1,19 +1,22 @@
<script lang="ts">
import type { PageData } from "../../routes/$types.js"; // Approximate type; adjust if needed
import type { PageData } from "../../routes/$types.js"; // Approximate type; adjust if needed
let { data, isWon }: { data: PageData; isWon: boolean } = $props();
let dailyVerse = $derived(data.dailyVerse);
let { data, isWon }: { data: PageData; isWon: boolean } = $props();
let dailyVerse = $derived(data.dailyVerse);
let displayReference = $derived(
dailyVerse.reference.replace(/^Psalms /, "Psalm ")
);
</script>
<div class="bg-gray-50 rounded-2xl shadow-xl p-8 sm:p-12 mb-8 sm:mb-12 w-full">
<blockquote
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
>
{dailyVerse.verseText}
</blockquote>
{#if isWon}
<p class="text-center text-lg text-green-600 font-bold mt-4">
{dailyVerse.reference}
</p>
{/if}
<div class="bg-gray-50 rounded-2xl shadow-xl p-8 sm:p-12 mb-4 sm:mb-12 w-full">
<blockquote
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
>
{dailyVerse.verseText}
</blockquote>
{#if isWon}
<p class="text-center text-lg! big-text text-green-600! font-bold mt-8">
{displayReference}
</p>
{/if}
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { getBookById, toOrdinal } from "$lib/utils/game";
import { getBookById, toOrdinal, getNextGradeMessage } from "$lib/utils/game";
interface StatsData {
solveRank: number;
@@ -128,20 +128,50 @@
</button>
{/if}
<p class="pt-6 big-text text-gray-100!">
{getNextGradeMessage(guessCount)}
</p>
<!-- Statistics Display -->
{#if statsData}
<div class="mt-6 space-y-2 text-lg" in:fade={{ delay: 800 }}>
<p class="font-regular">
You were the {toOrdinal(statsData.solveRank)} person to solve today.
</p>
<p class="font-regular">
You rank <span class="">{toOrdinal(statsData.guessRank)}</span> in guesses.
</p>
<p class="opacity-90">
Average: <span class="font-semibold"
>{Math.ceil(statsData.averageGuesses)}</span
> guesses
</p>
<div class="mt-6" in:fade={{ delay: 800 }}>
<div class="grid grid-cols-3 gap-4 gap-x-8 text-center">
<!-- Solve Rank Column -->
<div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black">
#{statsData.solveRank}
</div>
<div class="text-xs sm:text-sm opacity-90 mt-1">
You were the {toOrdinal(statsData.solveRank)} person to solve today
</div>
</div>
<!-- Guess Rank Column -->
<div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black">
{Math.round(
((statsData.totalSolves - statsData.guessRank + 1) /
statsData.totalSolves) *
100
)}%
</div>
<div class="text-xs sm:text-sm opacity-90 mt-1">
You ranked {toOrdinal(statsData.guessRank)} of {statsData.totalSolves}
total solves
</div>
</div>
<!-- Average Column -->
<div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black">
{statsData.averageGuesses}
</div>
<div class="text-xs sm:text-sm opacity-90 mt-1">
People guessed correctly after {statsData.averageGuesses} guesses on
average
</div>
</div>
</div>
</div>
{:else if !statsSubmitted}
<div class="mt-6 text-sm opacity-80">Submitting stats...</div>

View File

@@ -1,45 +1,30 @@
import type { DailyVerse } from '$lib/server/db/schema';
import { getBookById } from './bible';
import { getRandomVerses, formatReference } from './xml-bible';
type ApiVerse = Omit<DailyVerse, 'id' | 'date' | 'createdAt'>;
export async function fetchRandomVerse(): Promise<ApiVerse> {
// Step 1: Fetch random verse to get starting position
const randomRes = await fetch('https://bible-api.com/data/web/random');
if (!randomRes.ok) {
throw new Error(`Failed to fetch random verse: ${randomRes.status}`);
}
const randomData = await randomRes.json() as any;
const randomVerse = randomData.random_verse;
if (!randomVerse || !randomVerse.book_id) {
throw new Error('Invalid random verse data');
// Get 3 random verses from the local XML Bible
const verseData = getRandomVerses(3);
if (!verseData) {
throw new Error('Failed to get random verses from Bible XML');
}
const bookId = randomVerse.book_id as string;
const chapter = randomVerse.chapter as number;
const verse = randomVerse.verse as number;
const endVerse = verse + 2;
// Step 2: Fetch 3 consecutive verses starting from random
const rangeUrl = `https://bible-api.com/${bookId}${chapter}:${verse}-${endVerse}`;
const rangeRes = await fetch(rangeUrl);
if (!rangeRes.ok) {
throw new Error(`Failed to fetch verse range: ${rangeRes.status}`);
}
const rangeData = await rangeRes.json() as any;
const reference = rangeData.reference as string;
const verses = rangeData.verses as any[];
if (!verses || verses.length === 0) {
throw new Error('No verses in range');
}
const verseText = verses.map((v: any) => v.text).join(' ');
const { bookId, bookName, chapter, startVerse, endVerse, verses } = verseData;
// Validate bookId
if (!getBookById(bookId)) {
throw new Error(`Unknown book ID from API: ${bookId}`);
throw new Error(`Unknown book ID: ${bookId}`);
}
// Format the reference string (e.g., "Matthew 1:1-3")
const reference = formatReference(bookName, chapter, startVerse, endVerse);
// Join verses with spaces
const verseText = verses.join(' ');
return {
bookId,
reference,

View File

@@ -1,10 +1,26 @@
import type { BibleBook, Testament, BibleSection } from '$lib/types/bible';
import { bibleBooks } from '$lib/types/bible';
// Book number (1-66) to book ID mapping derived from bibleBooks order property
export const bookNumberToId: Record<number, string> = bibleBooks.reduce((acc, book) => {
acc[book.order] = book.id;
return acc;
}, {} as Record<number, string>);
// Book ID to book number mapping (reverse lookup)
export const bookIdToNumber: Record<string, number> = bibleBooks.reduce((acc, book) => {
acc[book.id] = book.order;
return acc;
}, {} as Record<string, number>);
export function getBookById(id: string): BibleBook | undefined {
return bibleBooks.find((book) => book.id === id);
}
export function getBookByNumber(number: number): BibleBook | undefined {
return bibleBooks.find((book) => book.order === number);
}
export function getBooksByTestament(testament: Testament): BibleBook[] {
return bibleBooks.filter((book) => book.testament === testament);
}

212
src/lib/server/xml-bible.ts Normal file
View File

@@ -0,0 +1,212 @@
import { XMLParser } from 'fast-xml-parser';
import { readFileSync } from 'fs';
import { join } from 'path';
import { bookNumberToId, getBookByNumber } from './bible';
// XML parser configuration
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
textNodeName: '_text',
parseAttributeValue: true,
trimValues: true,
isArray: (name) => ['chapter', 'verse'].includes(name)
});
// Cache for parsed Bible data to avoid re-reading the file
let cachedBible: any = null;
interface VerseData {
number: number;
_text: string;
}
interface ChapterData {
number: number;
verse: VerseData[];
}
interface BookData {
number: number;
chapter: ChapterData[];
}
interface TestamentData {
name: string;
book: BookData[];
}
interface BibleData {
bible: {
testament: TestamentData[];
};
}
/**
* Load and parse the Bible XML file
*/
function loadBibleXml(): BibleData {
if (cachedBible) {
return cachedBible;
}
const xmlPath = join(process.cwd(), 'EnglishNKJBible.xml');
const xmlContent = readFileSync(xmlPath, 'utf-8');
cachedBible = parser.parse(xmlContent) as BibleData;
return cachedBible;
}
/**
* Get a specific book from the Bible XML
*/
function getBook(bookNumber: number): BookData | null {
const bible = loadBibleXml();
// Old Testament books are 1-39, New Testament are 40-66
const testamentIndex = bookNumber <= 39 ? 0 : 1;
const testament = bible.bible.testament[testamentIndex];
if (!testament) {
return null;
}
// Find the book by number within the testament
const bookIndex = bookNumber <= 39 ? bookNumber - 1 : bookNumber - 40;
return testament.book[bookIndex] || null;
}
/**
* Get a specific chapter from a book
*/
function getChapter(bookNumber: number, chapterNumber: number): ChapterData | null {
const book = getBook(bookNumber);
if (!book) {
return null;
}
return book.chapter.find((ch) => ch.number === chapterNumber) || null;
}
/**
* Get the number of verses in a specific chapter
*/
function getVerseCount(bookNumber: number, chapterNumber: number): number {
const chapter = getChapter(bookNumber, chapterNumber);
return chapter ? chapter.verse.length : 0;
}
/**
* Get the number of chapters in a specific book
*/
function getChapterCount(bookNumber: number): number {
const book = getBook(bookNumber);
return book ? book.chapter.length : 0;
}
/**
* Extract consecutive verses from a specific location
*/
function extractVerses(
bookNumber: number,
chapterNumber: number,
startVerse: number,
count: number
): string[] {
const chapter = getChapter(bookNumber, chapterNumber);
if (!chapter) {
return [];
}
const verses: string[] = [];
for (let i = 0; i < count; i++) {
const verseIndex = startVerse - 1 + i; // Convert to 0-based index
if (verseIndex >= chapter.verse.length) {
break;
}
verses.push(chapter.verse[verseIndex]._text);
}
return verses;
}
/**
* Get a random book number (1-66)
*/
function getRandomBookNumber(): number {
return Math.floor(Math.random() * 66) + 1;
}
/**
* Get a random chapter number for a specific book
*/
function getRandomChapterNumber(bookNumber: number): number {
const chapterCount = getChapterCount(bookNumber);
return Math.floor(Math.random() * chapterCount) + 1;
}
/**
* Get a random starting verse number for a specific chapter
* Ensures there are enough verses for the requested count
*/
function getRandomStartVerse(bookNumber: number, chapterNumber: number, verseCount: number): number {
const totalVerses = getVerseCount(bookNumber, chapterNumber);
const maxStartVerse = Math.max(1, totalVerses - verseCount + 1);
return Math.floor(Math.random() * maxStartVerse) + 1;
}
/**
* Get a random set of verses from the Bible
* Returns 3 consecutive verses by default
*/
export function getRandomVerses(count: number = 3): {
bookId: string;
bookName: string;
chapter: number;
startVerse: number;
endVerse: number;
verses: string[];
} | null {
// Try up to 10 times to find a valid passage
for (let attempt = 0; attempt < 10; attempt++) {
const bookNumber = getRandomBookNumber();
const book = getBookByNumber(bookNumber);
if (!book) {
continue;
}
const chapterNumber = getRandomChapterNumber(bookNumber);
const verseCount = getVerseCount(bookNumber, chapterNumber);
// Skip chapters that don't have enough verses
if (verseCount < count) {
continue;
}
const startVerse = getRandomStartVerse(bookNumber, chapterNumber, count);
const verses = extractVerses(bookNumber, chapterNumber, startVerse, count);
if (verses.length === count) {
return {
bookId: book.id,
bookName: book.name,
chapter: chapterNumber,
startVerse,
endVerse: startVerse + count - 1,
verses
};
}
}
return null;
}
/**
* Format a reference string from verse data
*/
export function formatReference(bookName: string, chapter: number, startVerse: number, endVerse: number): string {
if (startVerse === endVerse) {
return `${bookName} ${chapter}:${startVerse}`;
}
return `${bookName} ${chapter}:${startVerse}-${endVerse}`;
}

View File

@@ -11,14 +11,23 @@ export function isAdjacent(id1: string, id2: string): boolean {
}
export function getGrade(numGuesses: number, popularity: number): string {
const difficulty = 14 - popularity;
const performanceScore = Math.max(0, 10 - numGuesses);
const totalScore = performanceScore + difficulty * 0.8;
if (totalScore >= 14) return "🟢 S";
if (totalScore >= 11) return "🟢 A";
if (totalScore >= 8) return "🟡 B";
if (totalScore >= 5) return "🟠 C";
return "🔴 C-";
if (numGuesses === 1) return "S+";
if (numGuesses === 2) return "A+";
if (numGuesses === 3) return "A";
if (numGuesses >= 4 && numGuesses <= 6) return "B+";
if (numGuesses >= 7 && numGuesses <= 10) return "B";
if (numGuesses >= 11 && numGuesses <= 15) return "C+";
return "C";
}
export function getNextGradeMessage(numGuesses: number): string {
if (numGuesses === 1) return "";
if (numGuesses === 2) return "Next grade: 1 guess or less";
if (numGuesses === 3) return "Next grade: 2 guesses or less";
if (numGuesses >= 4 && numGuesses <= 6) return "Next grade: 3 guesses or less";
if (numGuesses >= 7 && numGuesses <= 10) return "Next grade: 6 guesses or less";
if (numGuesses >= 11 && numGuesses <= 15) return "Next grade: 10 guesses or less";
return "Next grade: 15 guesses or less";
}
export function toOrdinal(n: number): string {

View File

@@ -125,7 +125,7 @@
$effect(() => {
if (!browser) return;
isDev = window.location.host === "192.168.0.42:5174";
isDev = window.location.host === "localhost:5173";
});
// Load saved guesses
@@ -335,6 +335,21 @@
}
});
}
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();
}
</script>
<svelte:head>
@@ -376,5 +391,13 @@
{#if isWon}
<Feedback />
{/if}
{#if isDev}
<button
onclick={clearLocalStorage}
class="mt-4 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-bold transition-colors"
>
Clear LocalStorage
</button>
{/if}
</div>
</div>

View File

@@ -9,3 +9,11 @@
html, body {
background: oklch(98.11% 0.02777 158.93);
}
.big-text {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: rgb(107 114 128);
font-weight: 700;
}

View File

@@ -0,0 +1,25 @@
import type { PageServerLoad } from './$types';
import { fetchRandomVerse } from '$lib/server/bible-api';
import { getBookById } from '$lib/server/bible';
export const load: PageServerLoad = async () => {
const apiVerse = await fetchRandomVerse();
// Create a dailyVerse-like object for VerseDisplay
const dailyVerse = {
id: 'debug-' + Date.now(),
date: new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' }),
bookId: apiVerse.bookId,
reference: apiVerse.reference,
verseText: apiVerse.verseText,
createdAt: new Date()
};
const correctBook = getBookById(dailyVerse.bookId) ?? null;
return {
dailyVerse,
correctBookId: dailyVerse.bookId,
correctBook
};
};

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import type { PageProps } from "./$types";
import VerseDisplay from "$lib/components/VerseDisplay.svelte";
let { data }: PageProps = $props();
let dailyVerse = $derived(data.dailyVerse);
</script>
<svelte:head>
<title>Random Verse - Bibdle</title>
</svelte:head>
<div class="min-h-screen bg-linear-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div class="max-w-4xl mx-auto">
<h1 class="text-4xl font-bold text-center text-gray-800 mb-8">
Random Verse Debug
</h1>
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Verse Details</h2>
<div class="space-y-2 text-gray-600">
<p><strong>Book ID:</strong> {dailyVerse.bookId}</p>
<p><strong>Reference:</strong> {dailyVerse.reference}</p>
<p><strong>Verse Text:</strong> {dailyVerse.verseText}</p>
</div>
</div>
<VerseDisplay {data} isWon={true} />
</div>
</div>