mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-02-04 10:54:44 -05:00
Visual fixes, typo fixes, and updated todo + metadata
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,3 +27,6 @@ vite.config.ts.timestamp-*
|
|||||||
|
|
||||||
llms-*
|
llms-*
|
||||||
|
|
||||||
|
engwebu_usfx.xml
|
||||||
|
embeddings-cache-L12.json
|
||||||
|
embeddings-cache-L6.json
|
||||||
|
|||||||
@@ -20271,7 +20271,7 @@
|
|||||||
<chapter number="56">
|
<chapter number="56">
|
||||||
<verse number="1">Thus says the Lord: “Keep justice, and do righteousness, For My salvation is about to come, And My righteousness to be revealed.</verse>
|
<verse number="1">Thus says the Lord: “Keep justice, and do righteousness, For My salvation is about to come, And My righteousness to be revealed.</verse>
|
||||||
<verse number="2">Blessed is the man who does this, And the son of man who lays hold on it; Who keeps from defiling the Sabbath, And keeps his hand from doing any evil.”</verse>
|
<verse number="2">Blessed is the man who does this, And the son of man who lays hold on it; Who keeps from defiling the Sabbath, And keeps his hand from doing any evil.”</verse>
|
||||||
<verse number="3">Do not let the son of the foreigner Who has joined himself to the LordSpeak, saying, “The Lord has utterly separated me from His people”; Nor let the eunuch say, “Here I am, a dry tree.”</verse>
|
<verse number="3">Do not let the son of the foreigner Who has joined himself to the Lord speak, saying, “The Lord has utterly separated me from His people”; Nor let the eunuch say, “Here I am, a dry tree.”</verse>
|
||||||
<verse number="4">For thus says the Lord: “To the eunuchs who keep My Sabbaths, And choose what pleases Me, And hold fast My covenant,</verse>
|
<verse number="4">For thus says the Lord: “To the eunuchs who keep My Sabbaths, And choose what pleases Me, And hold fast My covenant,</verse>
|
||||||
<verse number="5">Even to them I will give in My house And within My walls a place and a name Better than that of sons and daughters; I will give them an everlasting name That shall not be cut off.</verse>
|
<verse number="5">Even to them I will give in My house And within My walls a place and a name Better than that of sons and daughters; I will give them an everlasting name That shall not be cut off.</verse>
|
||||||
<verse number="6">“Also the sons of the foreigner Who join themselves to the Lord, to serve Him, And to love the name of the Lord, to be His servants— Everyone who keeps from defiling the Sabbath, And holds fast My covenant—</verse>
|
<verse number="6">“Also the sons of the foreigner Who join themselves to the Lord, to serve Him, And to love the name of the Lord, to be His servants— Everyone who keeps from defiling the Sabbath, And holds fast My covenant—</verse>
|
||||||
|
|||||||
@@ -6,13 +6,16 @@
|
|||||||
let displayReference = $derived(
|
let displayReference = $derived(
|
||||||
dailyVerse.reference.replace(/^Psalms /, "Psalm ")
|
dailyVerse.reference.replace(/^Psalms /, "Psalm ")
|
||||||
);
|
);
|
||||||
|
let displayVerseText = $derived(
|
||||||
|
dailyVerse.verseText.replace(/^([a-z])/, (c) => c.toUpperCase())
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bg-gray-50 rounded-2xl shadow-xl p-8 sm:p-12 mb-4 sm:mb-12 w-full">
|
<div class="bg-gray-50 rounded-2xl shadow-xl p-8 sm:p-12 mb-4 sm:mb-12 w-full">
|
||||||
<blockquote
|
<blockquote
|
||||||
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
|
class="text-xl sm:text-2xl font-triodion leading-relaxed text-gray-700 text-center"
|
||||||
>
|
>
|
||||||
{dailyVerse.verseText}
|
{displayVerseText}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
{#if isWon}
|
{#if isWon}
|
||||||
<p class="text-center text-lg! big-text text-green-600! font-bold mt-8">
|
<p class="text-center text-lg! big-text text-green-600! font-bold mt-8">
|
||||||
|
|||||||
@@ -167,8 +167,8 @@
|
|||||||
{statsData.averageGuesses}
|
{statsData.averageGuesses}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs sm:text-sm opacity-90 mt-1">
|
<div class="text-xs sm:text-sm opacity-90 mt-1">
|
||||||
People guessed correctly after {statsData.averageGuesses} guesses on
|
People guessed correctly after {statsData.averageGuesses}
|
||||||
average
|
{statsData.averageGuesses === 1 ? "guess" : "guesses"} on average
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,417 +1,421 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { bibleBooks, type BibleBook } from "$lib/types/bible";
|
import { bibleBooks, type BibleBook } from "$lib/types/bible";
|
||||||
|
|
||||||
import type { PageProps } from "./$types";
|
import type { PageProps } from "./$types";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
import VerseDisplay from "$lib/components/VerseDisplay.svelte";
|
import VerseDisplay from "$lib/components/VerseDisplay.svelte";
|
||||||
import SearchInput from "$lib/components/SearchInput.svelte";
|
import SearchInput from "$lib/components/SearchInput.svelte";
|
||||||
import GuessesTable from "$lib/components/GuessesTable.svelte";
|
import GuessesTable from "$lib/components/GuessesTable.svelte";
|
||||||
import CountdownTimer from "$lib/components/CountdownTimer.svelte";
|
import CountdownTimer from "$lib/components/CountdownTimer.svelte";
|
||||||
import WinScreen from "$lib/components/WinScreen.svelte";
|
import WinScreen from "$lib/components/WinScreen.svelte";
|
||||||
import Feedback from "$lib/components/Feedback.svelte";
|
import Feedback from "$lib/components/Feedback.svelte";
|
||||||
import TitleAnimation from "$lib/components/TitleAnimation.svelte";
|
import TitleAnimation from "$lib/components/TitleAnimation.svelte";
|
||||||
import { getGrade } from "$lib/utils/game";
|
import { getGrade } from "$lib/utils/game";
|
||||||
|
|
||||||
interface Guess {
|
interface Guess {
|
||||||
book: BibleBook;
|
book: BibleBook;
|
||||||
testamentMatch: boolean;
|
testamentMatch: boolean;
|
||||||
sectionMatch: boolean;
|
sectionMatch: boolean;
|
||||||
adjacent: boolean;
|
adjacent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
let dailyVerse = $derived(data.dailyVerse);
|
let dailyVerse = $derived(data.dailyVerse);
|
||||||
let correctBookId = $derived(data.correctBookId);
|
let correctBookId = $derived(data.correctBookId);
|
||||||
|
|
||||||
let guesses = $state<Guess[]>([]);
|
let guesses = $state<Guess[]>([]);
|
||||||
|
|
||||||
let searchQuery = $state("");
|
let searchQuery = $state("");
|
||||||
|
|
||||||
let copied = $state(false);
|
let copied = $state(false);
|
||||||
let isDev = $state(false);
|
let isDev = $state(false);
|
||||||
|
|
||||||
let anonymousId = $state("");
|
let anonymousId = $state("");
|
||||||
let statsSubmitted = $state(false);
|
let statsSubmitted = $state(false);
|
||||||
let statsData = $state<{
|
let statsData = $state<{
|
||||||
solveRank: number;
|
solveRank: number;
|
||||||
guessRank: number;
|
guessRank: number;
|
||||||
totalSolves: number;
|
totalSolves: number;
|
||||||
averageGuesses: number;
|
averageGuesses: number;
|
||||||
} | null>(null);
|
} | 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(
|
const currentDate = $derived(
|
||||||
new Date().toLocaleDateString("en-US", {
|
new Date().toLocaleDateString("en-US", {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
|
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
|
||||||
let grade = $derived(
|
let grade = $derived(
|
||||||
isWon
|
isWon
|
||||||
? getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0)
|
? getGrade(
|
||||||
: ""
|
guesses.length,
|
||||||
);
|
getBookById(correctBookId)?.popularity ?? 0,
|
||||||
|
)
|
||||||
|
: "",
|
||||||
|
);
|
||||||
|
|
||||||
function getBookById(id: string): BibleBook | undefined {
|
function getBookById(id: string): BibleBook | undefined {
|
||||||
return bibleBooks.find((b) => b.id === id);
|
return bibleBooks.find((b) => b.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAdjacent(id1: string, id2: string): boolean {
|
function isAdjacent(id1: string, id2: string): boolean {
|
||||||
const b1 = getBookById(id1);
|
const b1 = getBookById(id1);
|
||||||
const b2 = getBookById(id2);
|
const b2 = getBookById(id2);
|
||||||
return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1);
|
return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitGuess(bookId: string) {
|
function submitGuess(bookId: string) {
|
||||||
if (guesses.some((g) => g.book.id === bookId)) return;
|
if (guesses.some((g) => g.book.id === bookId)) return;
|
||||||
|
|
||||||
const book = getBookById(bookId);
|
const book = getBookById(bookId);
|
||||||
if (!book) return;
|
if (!book) return;
|
||||||
|
|
||||||
const correctBook = getBookById(correctBookId);
|
const correctBook = getBookById(correctBookId);
|
||||||
if (!correctBook) return;
|
if (!correctBook) return;
|
||||||
|
|
||||||
const testamentMatch = book.testament === correctBook.testament;
|
const testamentMatch = book.testament === correctBook.testament;
|
||||||
const sectionMatch = book.section === correctBook.section;
|
const sectionMatch = book.section === correctBook.section;
|
||||||
const adjacent = isAdjacent(book.id, correctBookId);
|
const adjacent = isAdjacent(book.id, correctBookId);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`
|
`Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
guesses = [
|
guesses = [
|
||||||
{
|
{
|
||||||
book,
|
book,
|
||||||
testamentMatch,
|
testamentMatch,
|
||||||
sectionMatch,
|
sectionMatch,
|
||||||
adjacent,
|
adjacent,
|
||||||
},
|
},
|
||||||
...guesses,
|
...guesses,
|
||||||
];
|
];
|
||||||
|
|
||||||
searchQuery = "";
|
searchQuery = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateUUID(): string {
|
function generateUUID(): string {
|
||||||
// Try native randomUUID if available
|
// Try native randomUUID if available
|
||||||
if (typeof window.crypto.randomUUID === "function") {
|
if (typeof window.crypto.randomUUID === "function") {
|
||||||
return window.crypto.randomUUID();
|
return window.crypto.randomUUID();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback UUID v4 generator for older browsers
|
// Fallback UUID v4 generator for older browsers
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||||
const r = window.crypto.getRandomValues(new Uint8Array(1))[0] % 16 | 0;
|
const r =
|
||||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
window.crypto.getRandomValues(new Uint8Array(1))[0] % 16 | 0;
|
||||||
return v.toString(16);
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||||
});
|
return v.toString(16);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getOrCreateAnonymousId(): string {
|
function getOrCreateAnonymousId(): string {
|
||||||
if (!browser) return "";
|
if (!browser) return "";
|
||||||
const key = "bibdle-anonymous-id";
|
const key = "bibdle-anonymous-id";
|
||||||
let id = localStorage.getItem(key);
|
let id = localStorage.getItem(key);
|
||||||
if (!id) {
|
if (!id) {
|
||||||
id = generateUUID();
|
id = generateUUID();
|
||||||
localStorage.setItem(key, id);
|
localStorage.setItem(key, id);
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize anonymous ID
|
// Initialize anonymous ID
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
anonymousId = getOrCreateAnonymousId();
|
anonymousId = getOrCreateAnonymousId();
|
||||||
const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`;
|
const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`;
|
||||||
statsSubmitted = localStorage.getItem(statsKey) === "true";
|
statsSubmitted = localStorage.getItem(statsKey) === "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
isDev = window.location.host === "localhost:5173";
|
isDev = window.location.host === "localhost:5173";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load saved guesses
|
// Load saved guesses
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
const key = `bibdle-guesses-${dailyVerse.date}`;
|
const key = `bibdle-guesses-${dailyVerse.date}`;
|
||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage.getItem(key);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
let savedIds: string[] = JSON.parse(saved);
|
let savedIds: string[] = JSON.parse(saved);
|
||||||
savedIds = Array.from(new Set(savedIds));
|
savedIds = Array.from(new Set(savedIds));
|
||||||
guesses = savedIds.map((bookId: string) => {
|
guesses = savedIds.map((bookId: string) => {
|
||||||
const book = getBookById(bookId)!;
|
const book = getBookById(bookId)!;
|
||||||
const correctBook = getBookById(correctBookId)!;
|
const correctBook = getBookById(correctBookId)!;
|
||||||
const testamentMatch = book.testament === correctBook.testament;
|
const testamentMatch = book.testament === correctBook.testament;
|
||||||
const sectionMatch = book.section === correctBook.section;
|
const sectionMatch = book.section === correctBook.section;
|
||||||
const adjacent = isAdjacent(bookId, correctBookId);
|
const adjacent = isAdjacent(bookId, correctBookId);
|
||||||
return {
|
return {
|
||||||
book,
|
book,
|
||||||
testamentMatch,
|
testamentMatch,
|
||||||
sectionMatch,
|
sectionMatch,
|
||||||
adjacent,
|
adjacent,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`bibdle-guesses-${dailyVerse.date}`,
|
`bibdle-guesses-${dailyVerse.date}`,
|
||||||
JSON.stringify(guesses.map((g) => g.book.id))
|
JSON.stringify(guesses.map((g) => g.book.id)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-submit stats when user wins
|
// Auto-submit stats when user wins
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.log("Stats effect triggered:", {
|
console.log("Stats effect triggered:", {
|
||||||
browser,
|
browser,
|
||||||
isWon,
|
isWon,
|
||||||
anonymousId,
|
anonymousId,
|
||||||
statsSubmitted,
|
statsSubmitted,
|
||||||
statsData,
|
statsData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!browser || !isWon || !anonymousId) {
|
if (!browser || !isWon || !anonymousId) {
|
||||||
console.log("Basic conditions not met");
|
console.log("Basic conditions not met");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statsSubmitted && !statsData) {
|
if (statsSubmitted && !statsData) {
|
||||||
console.log("Fetching existing stats...");
|
console.log("Fetching existing stats...");
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`
|
`/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`,
|
||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Stats response:", result);
|
console.log("Stats response:", result);
|
||||||
|
|
||||||
if (result.success && result.stats) {
|
if (result.success && result.stats) {
|
||||||
console.log("Setting stats data:", result.stats);
|
console.log("Setting stats data:", result.stats);
|
||||||
statsData = result.stats;
|
statsData = result.stats;
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`bibdle-stats-submitted-${dailyVerse.date}`,
|
`bibdle-stats-submitted-${dailyVerse.date}`,
|
||||||
"true"
|
"true",
|
||||||
);
|
);
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
console.error("Server error:", result.error);
|
console.error("Server error:", result.error);
|
||||||
} else {
|
} else {
|
||||||
console.error("Unexpected response format:", result);
|
console.error("Unexpected response format:", result);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Stats fetch failed:", err);
|
console.error("Stats fetch failed:", err);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Submitting stats...");
|
console.log("Submitting stats...");
|
||||||
|
|
||||||
async function submitStats() {
|
async function submitStats() {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
anonymousId,
|
anonymousId,
|
||||||
date: dailyVerse.date,
|
date: dailyVerse.date,
|
||||||
guessCount: guesses.length,
|
guessCount: guesses.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Sending POST request with:", payload);
|
console.log("Sending POST request with:", payload);
|
||||||
|
|
||||||
const response = await fetch("/api/submit-completion", {
|
const response = await fetch("/api/submit-completion", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Stats response:", result);
|
console.log("Stats response:", result);
|
||||||
|
|
||||||
if (result.success && result.stats) {
|
if (result.success && result.stats) {
|
||||||
console.log("Setting stats data:", result.stats);
|
console.log("Setting stats data:", result.stats);
|
||||||
statsData = result.stats;
|
statsData = result.stats;
|
||||||
statsSubmitted = true;
|
statsSubmitted = true;
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`bibdle-stats-submitted-${dailyVerse.date}`,
|
`bibdle-stats-submitted-${dailyVerse.date}`,
|
||||||
"true"
|
"true",
|
||||||
);
|
);
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
console.error("Server error:", result.error);
|
console.error("Server error:", result.error);
|
||||||
} else {
|
} else {
|
||||||
console.error("Unexpected response format:", result);
|
console.error("Unexpected response format:", result);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Stats submission failed:", err);
|
console.error("Stats submission failed:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitStats();
|
submitStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
function generateShareText(): string {
|
function generateShareText(): string {
|
||||||
const emojis = guesses
|
const emojis = guesses
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((guess) => {
|
.map((guess) => {
|
||||||
if (guess.book.id === correctBookId) return "✅";
|
if (guess.book.id === correctBookId) return "✅";
|
||||||
if (guess.adjacent) return "‼️";
|
if (guess.adjacent) return "‼️";
|
||||||
if (guess.sectionMatch) return "🟩";
|
if (guess.sectionMatch) return "🟩";
|
||||||
if (guess.testamentMatch) return "🟧";
|
if (guess.testamentMatch) return "🟧";
|
||||||
return "🟥";
|
return "🟥";
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
const formattedDate = dateFormatter.format(
|
const formattedDate = dateFormatter.format(
|
||||||
new Date(`${dailyVerse.date}T00:00:00`)
|
new Date(`${dailyVerse.date}T00:00:00`),
|
||||||
);
|
);
|
||||||
const siteUrl = window.location.origin;
|
const siteUrl = window.location.origin;
|
||||||
return [
|
return [
|
||||||
`📖 Bibdle | ${formattedDate} 📖`,
|
`📖 Bibdle | ${formattedDate} 📖`,
|
||||||
`${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`,
|
`${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`,
|
||||||
`${emojis}`,
|
`${emojis}`,
|
||||||
siteUrl,
|
siteUrl,
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function share() {
|
async function share() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
const shareText = generateShareText();
|
const shareText = generateShareText();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ("share" in navigator) {
|
if ("share" in navigator) {
|
||||||
await (navigator as any).share({ text: shareText });
|
await (navigator as any).share({ text: shareText });
|
||||||
} else {
|
} else {
|
||||||
await (navigator as any).clipboard.writeText(shareText);
|
await (navigator as any).clipboard.writeText(shareText);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Share failed:", err);
|
console.error("Share failed:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard() {
|
async function copyToClipboard() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
|
|
||||||
const shareText = generateShareText();
|
const shareText = generateShareText();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await (navigator as any).clipboard.writeText(shareText);
|
await (navigator as any).clipboard.writeText(shareText);
|
||||||
copied = true;
|
copied = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied = false;
|
copied = false;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Copy to clipboard failed:", err);
|
console.error("Copy to clipboard failed:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
if (copied || !browser) return;
|
if (copied || !browser) return;
|
||||||
const useClipboard = !("share" in navigator);
|
const useClipboard = !("share" in navigator);
|
||||||
if (useClipboard) {
|
if (useClipboard) {
|
||||||
copied = true;
|
copied = true;
|
||||||
}
|
}
|
||||||
share()
|
share()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (useClipboard) {
|
if (useClipboard) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied = false;
|
copied = false;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (useClipboard) {
|
if (useClipboard) {
|
||||||
copied = false;
|
copied = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLocalStorage() {
|
function clearLocalStorage() {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
// Clear all bibdle-related localStorage items
|
// Clear all bibdle-related localStorage items
|
||||||
const keysToRemove: string[] = [];
|
const keysToRemove: string[] = [];
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if (key && key.startsWith("bibdle-")) {
|
if (key && key.startsWith("bibdle-")) {
|
||||||
keysToRemove.push(key);
|
keysToRemove.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||||
// Reload the page to reset state
|
// Reload the page to reset state
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Bibdle — A daily bible game{isDev ? " (dev)" : ""}</title>
|
<title>Bibdle — A daily bible game{isDev ? " (dev)" : ""}</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="A Wordle-inspired Bible game (short for Bible Daily)"
|
content="Guess which book of the Bible a verse comes from."
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-dvh md:bg-linear-to-br md:from-blue-50 md:to-indigo-200 py-8">
|
<div class="min-h-dvh md:bg-linear-to-br md:from-blue-50 md:to-indigo-200 py-8">
|
||||||
<div class="w-full max-w-3xl mx-auto px-4">
|
<div class="w-full max-w-3xl mx-auto px-4">
|
||||||
<h1
|
<h1
|
||||||
class="text-3xl md:text-4xl font-bold text-center uppercase text-gray-600 drop-shadow-2xl tracking-widest p-4"
|
class="text-3xl md:text-4xl font-bold text-center uppercase text-gray-600 drop-shadow-2xl tracking-widest p-4"
|
||||||
>
|
>
|
||||||
<TitleAnimation />
|
<TitleAnimation />
|
||||||
<div class="font-normal"></div>
|
<div class="font-normal"></div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<span class="big-text"
|
<span class="big-text"
|
||||||
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
|
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VerseDisplay {data} {isWon} />
|
<VerseDisplay {data} {isWon} />
|
||||||
|
|
||||||
{#if !isWon}
|
{#if !isWon}
|
||||||
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
|
<SearchInput bind:searchQuery {guessedIds} {submitGuess} />
|
||||||
{:else}
|
{:else}
|
||||||
<WinScreen
|
<WinScreen
|
||||||
{grade}
|
{grade}
|
||||||
{statsData}
|
{statsData}
|
||||||
{correctBookId}
|
{correctBookId}
|
||||||
{handleShare}
|
{handleShare}
|
||||||
{copyToClipboard}
|
{copyToClipboard}
|
||||||
bind:copied
|
bind:copied
|
||||||
{statsSubmitted}
|
{statsSubmitted}
|
||||||
guessCount={guesses.length}
|
guessCount={guesses.length}
|
||||||
/>
|
/>
|
||||||
<CountdownTimer />
|
<CountdownTimer />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<GuessesTable {guesses} {correctBookId} />
|
<GuessesTable {guesses} {correctBookId} />
|
||||||
{#if isWon}
|
{#if isWon}
|
||||||
<Feedback />
|
<Feedback />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isDev}
|
{#if isDev}
|
||||||
<button
|
<button
|
||||||
onclick={clearLocalStorage}
|
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"
|
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
|
Clear LocalStorage
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
todo.md
34
todo.md
@@ -1,20 +1,36 @@
|
|||||||
|
# in progress
|
||||||
|
|
||||||
|
|
||||||
|
- root menu: classic / imposter mode / impossible mode (complete today's classic and imposter modes to unlock)
|
||||||
|
|
||||||
|
|
||||||
# todo
|
# todo
|
||||||
|
|
||||||
- Difficulty levels
|
- impossible mode (1904 greek bible) three guesses only.
|
||||||
- difficult mode (guess old or new testament, first try _only_)
|
- share both classic and impossible mode with both buttons
|
||||||
- impossible mode (1894 scrivener koine greek NT or some hebrew version for OT) three guesses only
|
|
||||||
|
|
||||||
- "login to see your stats, unlock practice mode, and more"
|
- 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.
|
||||||
|
|
||||||
|
- add login + saved stats + streak etc.
|
||||||
|
|
||||||
|
- add deuterocanonical books
|
||||||
|
|
||||||
|
<!-- Login features -->
|
||||||
- Practice mode: Unlimited verses
|
- Practice mode: Unlimited verses
|
||||||
- Create public or private leaderboards
|
- Create public or private leaderboards
|
||||||
- Passport book with badges:
|
- Passport book with badges:
|
||||||
|
|
||||||
- Guess each Gospel first try
|
- Guess each Gospel first try
|
||||||
- "Guessed all Gospels", "Perfect week", "Old Testament expert"
|
- "Guessed all Gospels", "Perfect week", "Old Testament expert"
|
||||||
- Theologian: Guess each book first try
|
- Theologian: Guess each book first try
|
||||||
|
|
||||||
- instructions
|
|
||||||
|
|
||||||
|
- difficult mode (guess old or new testament, first try _only_) (???)
|
||||||
|
|
||||||
# places to send
|
# places to send
|
||||||
|
|
||||||
@@ -36,6 +52,12 @@ I created Bibdle from a combination of two things. The first is my lifelong desi
|
|||||||
|
|
||||||
# done
|
# done
|
||||||
|
|
||||||
|
## december 26th
|
||||||
|
|
||||||
|
- created embeddings for every bible verse (verse similarity finder)
|
||||||
|
- failed at having AI write a USFX format parser
|
||||||
|
- found a npm library for parsing USFX
|
||||||
|
|
||||||
## december 23rd
|
## december 23rd
|
||||||
|
|
||||||
- switched to local copy of NKJV
|
- switched to local copy of NKJV
|
||||||
|
|||||||
Reference in New Issue
Block a user