mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-02-04 02:44:43 -05:00
Added chapter guess challenge
This commit is contained in:
216
src/lib/components/ChapterGuess.svelte
Normal file
216
src/lib/components/ChapterGuess.svelte
Normal file
@@ -0,0 +1,216 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import { browser } from "$app/environment";
|
||||
import Container from "./Container.svelte";
|
||||
|
||||
interface Props {
|
||||
reference: string;
|
||||
bookId: string;
|
||||
onCompleted?: () => void;
|
||||
}
|
||||
|
||||
let { reference, bookId, onCompleted }: Props = $props();
|
||||
|
||||
// Parse the chapter from the reference (e.g., "John 3:16" -> 3)
|
||||
function parseChapterFromReference(ref: string): number {
|
||||
const match = ref.match(/\s(\d+):/);
|
||||
return match ? parseInt(match[1], 10) : 1;
|
||||
}
|
||||
|
||||
// Get the number of chapters for a book
|
||||
function getChapterCount(bookId: string): number {
|
||||
const chapterCounts: Record<string, number> = {
|
||||
GEN: 50,
|
||||
EXO: 40,
|
||||
LEV: 27,
|
||||
NUM: 36,
|
||||
DEU: 34,
|
||||
JOS: 24,
|
||||
JDG: 21,
|
||||
RUT: 4,
|
||||
"1SA": 31,
|
||||
"2SA": 24,
|
||||
"1KI": 22,
|
||||
"2KI": 25,
|
||||
"1CH": 29,
|
||||
"2CH": 36,
|
||||
EZR: 10,
|
||||
NEH: 13,
|
||||
EST: 10,
|
||||
JOB: 42,
|
||||
PSA: 150,
|
||||
PRO: 31,
|
||||
ECC: 12,
|
||||
SNG: 8,
|
||||
ISA: 66,
|
||||
JER: 52,
|
||||
LAM: 5,
|
||||
EZK: 48,
|
||||
DAN: 12,
|
||||
HOS: 14,
|
||||
JOL: 3,
|
||||
AMO: 9,
|
||||
OBA: 1,
|
||||
JON: 4,
|
||||
MIC: 7,
|
||||
NAM: 3,
|
||||
HAB: 3,
|
||||
ZEP: 3,
|
||||
HAG: 2,
|
||||
ZEC: 14,
|
||||
MAL: 4,
|
||||
MAT: 28,
|
||||
MRK: 16,
|
||||
LUK: 24,
|
||||
JHN: 21,
|
||||
ACT: 28,
|
||||
ROM: 16,
|
||||
"1CO": 16,
|
||||
"2CO": 13,
|
||||
GAL: 6,
|
||||
EPH: 6,
|
||||
PHP: 4,
|
||||
COL: 4,
|
||||
"1TH": 5,
|
||||
"2TH": 3,
|
||||
"1TI": 6,
|
||||
"2TI": 4,
|
||||
TIT: 3,
|
||||
PHM: 1,
|
||||
HEB: 13,
|
||||
JAS: 5,
|
||||
"1PE": 5,
|
||||
"2PE": 3,
|
||||
"1JN": 5,
|
||||
"2JN": 1,
|
||||
"3JN": 1,
|
||||
JUD: 1,
|
||||
REV: 22,
|
||||
};
|
||||
return chapterCounts[bookId] || 1;
|
||||
}
|
||||
|
||||
// Generate 6 random chapter options including the correct one
|
||||
function generateChapterOptions(
|
||||
correctChapter: number,
|
||||
totalChapters: number
|
||||
): number[] {
|
||||
const options = new Set<number>();
|
||||
options.add(correctChapter);
|
||||
|
||||
if (totalChapters >= 6) {
|
||||
while (options.size < 6) {
|
||||
const randomChapter = Math.floor(Math.random() * totalChapters) + 1;
|
||||
options.add(randomChapter);
|
||||
}
|
||||
} else {
|
||||
while (options.size < 6) {
|
||||
const randomChapter = Math.floor(Math.random() * 10) + 1;
|
||||
options.add(randomChapter);
|
||||
}
|
||||
}
|
||||
return Array.from(options).sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
let correctChapter = $derived(parseChapterFromReference(reference));
|
||||
let totalChapters = $derived(getChapterCount(bookId));
|
||||
let chapterOptions = $state<number[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
if (chapterOptions.length === 0) {
|
||||
chapterOptions = generateChapterOptions(correctChapter, totalChapters);
|
||||
}
|
||||
});
|
||||
|
||||
let selectedChapter = $state<number | null>(null);
|
||||
let hasAnswered = $state(false);
|
||||
|
||||
// Load saved state from localStorage
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
const key = `bibdle-chapter-guess-${reference}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
selectedChapter = data.selectedChapter;
|
||||
hasAnswered = data.hasAnswered;
|
||||
chapterOptions = data.chapterOptions ?? [];
|
||||
}
|
||||
});
|
||||
|
||||
// Save state to localStorage whenever options are generated or answer given
|
||||
$effect(() => {
|
||||
if (!browser || chapterOptions.length === 0) return;
|
||||
const key = `bibdle-chapter-guess-${reference}`;
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({ selectedChapter, hasAnswered, chapterOptions })
|
||||
);
|
||||
});
|
||||
|
||||
function handleChapterSelect(chapter: number) {
|
||||
if (hasAnswered) return;
|
||||
selectedChapter = chapter;
|
||||
hasAnswered = true;
|
||||
if (onCompleted) {
|
||||
onCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
let isCorrect = $derived(
|
||||
selectedChapter !== null && selectedChapter === correctChapter
|
||||
);
|
||||
</script>
|
||||
|
||||
<Container
|
||||
class="w-full p-6 sm:p-8 bg-linear-to-br from-yellow-100/80 to-amber-200/80 text-gray-800 shadow-md"
|
||||
>
|
||||
<div class="text-center">
|
||||
<p class="text-xl sm:text-2xl font-bold mb-2">Bonus Challenge</p>
|
||||
<p class="text-sm sm:text-base opacity-80 mb-6">
|
||||
Guess the chapter for an even higher grade
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3 sm:gap-4 max-w-md mx-auto mb-6">
|
||||
{#each chapterOptions as chapter}
|
||||
<button
|
||||
onclick={() => handleChapterSelect(chapter)}
|
||||
disabled={hasAnswered}
|
||||
class={`
|
||||
aspect-square text-2xl sm:text-3xl font-bold rounded-xl
|
||||
transition-all duration-300 border-2
|
||||
${
|
||||
hasAnswered
|
||||
? chapter === correctChapter
|
||||
? "bg-green-500 text-white border-green-600 shadow-lg"
|
||||
: selectedChapter === chapter
|
||||
? isCorrect
|
||||
? "bg-green-500 text-white border-green-600 shadow-lg"
|
||||
: "bg-red-400 text-white border-red-500"
|
||||
: "bg-white/30 text-gray-400 border-gray-300 opacity-40"
|
||||
: "bg-white/80 hover:bg-white text-gray-800 border-gray-300 hover:border-amber-400 hover:shadow-md cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{chapter}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasAnswered}
|
||||
<p
|
||||
class="text-xl sm:text-2xl font-bold mb-2"
|
||||
class:text-green-600={isCorrect}
|
||||
class:text-red-600={!isCorrect}
|
||||
>
|
||||
{isCorrect ? "✓ Correct!" : "✗ Incorrect"}
|
||||
</p>
|
||||
<p class="text-sm opacity-80">
|
||||
The verse is from chapter {correctChapter}
|
||||
</p>
|
||||
{#if isCorrect}
|
||||
<p class="text-lg font-bold text-amber-600 mt-2">Grade: S++</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Container>
|
||||
@@ -16,16 +16,55 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Type to guess a book (e.g. 'Genesis', 'John')..."
|
||||
class="w-full p-4 sm:p-6 border-2 border-gray-200 rounded-2xl text-base sm:text-lg md:text-xl focus:outline-none focus:border-blue-500 focus:ring-4 focus:ring-blue-100 transition-all shadow-lg"
|
||||
onkeydown={handleKeydown}
|
||||
/>
|
||||
<div class="relative">
|
||||
<div class="relative">
|
||||
<svg
|
||||
class="absolute left-4 sm:left-6 top-1/2 transform -translate-y-1/2 w-5 h-5 sm:w-6 sm:h-6 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Type to guess a book (e.g. 'Genesis', 'John')..."
|
||||
class="w-full pl-12 sm:pl-16 p-4 sm:p-6 border-2 border-gray-500 rounded-2xl text-base sm:text-lg md:text-xl focus:outline-none focus:border-blue-600 focus:ring-4 focus:ring-blue-200 transition-all bg-white"
|
||||
onkeydown={handleKeydown}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button
|
||||
class="absolute right-4 sm:right-6 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
onclick={() => (searchQuery = "")}
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 sm:w-6 sm:h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if searchQuery && filteredBooks.length > 0}
|
||||
<ul
|
||||
class="mt-4 max-h-60 sm:max-h-80 overflow-y-auto bg-white border border-gray-200 rounded-2xl shadow-lg"
|
||||
class="mt-4 max-h-60 sm:max-h-80 overflow-y-auto bg-white border border-gray-300 rounded-2xl shadow-xl"
|
||||
>
|
||||
{#each filteredBooks as book (book.id)}
|
||||
<li>
|
||||
|
||||
@@ -2,17 +2,25 @@
|
||||
import type { PageData } from "../../routes/$types.js"; // Approximate type; adjust if needed
|
||||
import Container from "./Container.svelte";
|
||||
|
||||
let { data, isWon }: { data: PageData; isWon: boolean } = $props();
|
||||
let {
|
||||
data,
|
||||
isWon,
|
||||
blurChapter = false,
|
||||
}: { data: PageData; isWon: boolean; blurChapter?: boolean } = $props();
|
||||
let dailyVerse = $derived(data.dailyVerse);
|
||||
let displayReference = $derived(
|
||||
dailyVerse.reference.replace(/^Psalms /, "Psalm ")
|
||||
blurChapter
|
||||
? dailyVerse.reference
|
||||
.replace(/^Psalms /, "Psalm ")
|
||||
.replace(/\s(\d+):/, " ?:")
|
||||
: dailyVerse.reference.replace(/^Psalms /, "Psalm ")
|
||||
);
|
||||
let displayVerseText = $derived(
|
||||
dailyVerse.verseText.replace(/^([a-z])/, (c) => c.toUpperCase())
|
||||
);
|
||||
</script>
|
||||
|
||||
<Container class="w-full p-8 sm:p-12">
|
||||
<Container class="w-full p-8 sm:p-12 bg-white/70">
|
||||
<blockquote
|
||||
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import Container from "./Container.svelte";
|
||||
import CountdownTimer from "./CountdownTimer.svelte";
|
||||
import ChapterGuess from "./ChapterGuess.svelte";
|
||||
|
||||
interface StatsData {
|
||||
solveRank: number;
|
||||
@@ -26,6 +27,8 @@
|
||||
copied = $bindable(false),
|
||||
statsSubmitted,
|
||||
guessCount,
|
||||
reference,
|
||||
onChapterGuessCompleted,
|
||||
} = $props();
|
||||
|
||||
let bookName = $derived(getBookById(correctBookId)?.name ?? "");
|
||||
@@ -128,11 +131,22 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="pt-6 big-text text-gray-700!">
|
||||
{getNextGradeMessage(guessCount)}
|
||||
</p>
|
||||
{#if guessCount !== 1}
|
||||
<p class="pt-6 big-text text-gray-700!">
|
||||
{getNextGradeMessage(guessCount)}
|
||||
</p>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
<!-- S++ Bonus Challenge for first try -->
|
||||
{#if guessCount === 1}
|
||||
<ChapterGuess
|
||||
{reference}
|
||||
bookId={correctBookId}
|
||||
onCompleted={onChapterGuessCompleted}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<CountdownTimer />
|
||||
|
||||
<!-- Statistics Display -->
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
let copied = $state(false);
|
||||
let isDev = $state(false);
|
||||
let chapterGuessCompleted = $state(false);
|
||||
let chapterCorrect = $state(false);
|
||||
|
||||
let anonymousId = $state("");
|
||||
let statsSubmitted = $state(false);
|
||||
@@ -54,9 +56,14 @@
|
||||
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
|
||||
let grade = $derived(
|
||||
isWon
|
||||
? getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0)
|
||||
? guesses.length === 1 && chapterCorrect
|
||||
? "S++"
|
||||
: getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0)
|
||||
: ""
|
||||
);
|
||||
let blurChapter = $derived(
|
||||
isWon && guesses.length === 1 && !chapterGuessCompleted
|
||||
);
|
||||
|
||||
function getBookById(id: string): BibleBook | undefined {
|
||||
return bibleBooks.find((b) => b.id === id);
|
||||
@@ -79,7 +86,7 @@
|
||||
|
||||
const testamentMatch = book.testament === correctBook.testament;
|
||||
const sectionMatch = book.section === correctBook.section;
|
||||
const adjacent = isAdjacent(book.id, correctBookId);
|
||||
const adjacent = isAdjacent(bookId, correctBookId);
|
||||
|
||||
console.log(
|
||||
`Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`
|
||||
@@ -141,6 +148,17 @@
|
||||
anonymousId = getOrCreateAnonymousId();
|
||||
const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`;
|
||||
statsSubmitted = localStorage.getItem(statsKey) === "true";
|
||||
const chapterGuessKey = `bibdle-chapter-guess-${dailyVerse.reference}`;
|
||||
chapterGuessCompleted = localStorage.getItem(chapterGuessKey) !== null;
|
||||
if (chapterGuessCompleted) {
|
||||
const saved = localStorage.getItem(chapterGuessKey);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
const match = dailyVerse.reference.match(/\s(\d+):/);
|
||||
const correctChapter = match ? parseInt(match[1], 10) : 1;
|
||||
chapterCorrect = data.selectedChapter === correctChapter;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -307,8 +325,8 @@
|
||||
const siteUrl = window.location.origin;
|
||||
return [
|
||||
`📖 Bibdle | ${formattedDate} 📖`,
|
||||
`${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`,
|
||||
`${emojis}`,
|
||||
`${grade} (${guesses.length} ${guesses.length === 1 ? "guess" : "guesses"})`,
|
||||
`${emojis}${guesses.length === 1 && chapterCorrect ? " ⭐" : ""}`,
|
||||
siteUrl,
|
||||
].join("\n");
|
||||
}
|
||||
@@ -408,7 +426,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<VerseDisplay {data} {isWon} />
|
||||
<VerseDisplay {data} {isWon} {blurChapter} />
|
||||
|
||||
{#if !isWon}
|
||||
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
|
||||
@@ -422,6 +440,18 @@
|
||||
bind:copied
|
||||
{statsSubmitted}
|
||||
guessCount={guesses.length}
|
||||
reference={dailyVerse.reference}
|
||||
onChapterGuessCompleted={() => {
|
||||
chapterGuessCompleted = true;
|
||||
const key = `bibdle-chapter-guess-${dailyVerse.reference}`;
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
const match = dailyVerse.reference.match(/\s(\d+):/);
|
||||
const correctChapter = match ? parseInt(match[1], 10) : 1;
|
||||
chapterCorrect = data.selectedChapter === correctChapter;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user