major styling and spacing

This commit is contained in:
George Powell
2026-01-04 01:25:49 -05:00
parent a91a5af014
commit 0f6870344f
8 changed files with 249 additions and 214 deletions

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
class?: string;
}
let { children, class: className = "" }: Props = $props();
</script>
<div
class="inline-flex flex-col items-center bg-white/50 backdrop-blur-sm rounded-2xl border border-white/50 shadow-sm {className}"
>
{@render children()}
</div>

View File

@@ -1,90 +1,88 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
let timeUntilNext = $state(""); let timeUntilNext = $state("");
let intervalId: number | null = null; let intervalId: number | null = null;
function calculateTimeUntilFivePM(): string { function calculateTimeUntilFivePM(): string {
const now = new Date(); const now = new Date();
const target = new Date(now); const target = new Date(now);
// Set target to 5:00 PM today // Set target to 5:00 PM today
target.setHours(17, 0, 0, 0); target.setHours(17, 0, 0, 0);
// If it's already past 5:00 PM, set target to tomorrow 5:00 PM // If it's already past 5:00 PM, set target to tomorrow 5:00 PM
if (now.getTime() >= target.getTime()) { if (now.getTime() >= target.getTime()) {
target.setDate(target.getDate() + 1); target.setDate(target.getDate() + 1);
} }
const diff = target.getTime() - now.getTime(); const diff = target.getTime() - now.getTime();
if (diff <= 0) { if (diff <= 0) {
return "00:00:00"; return "00:00:00";
} }
const hours = Math.floor(diff / (1000 * 60 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000); const seconds = Math.floor((diff % (1000 * 60)) / 1000);
return `${hours.toString().padStart(2, "0")}h ${minutes return `${hours.toString().padStart(2, "0")}h ${minutes
.toString() .toString()
.padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`; .padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
} }
function calculateTimeUntilMidnight(): string { function calculateTimeUntilMidnight(): string {
const now = new Date(); const now = new Date();
const target = new Date(now); const target = new Date(now);
// Set target to midnight today // Set target to midnight today
target.setHours(0, 0, 0, 0); target.setHours(0, 0, 0, 0);
// If it's already past midnight, set target to tomorrow midnight // If it's already past midnight, set target to tomorrow midnight
if (now.getTime() >= target.getTime()) { if (now.getTime() >= target.getTime()) {
target.setDate(target.getDate() + 1); target.setDate(target.getDate() + 1);
} }
const diff = target.getTime() - now.getTime(); const diff = target.getTime() - now.getTime();
if (diff <= 0) { if (diff <= 0) {
return "00:00:00"; return "00:00:00";
} }
const hours = Math.floor(diff / (1000 * 60 * 60)); const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000); const seconds = Math.floor((diff % (1000 * 60)) / 1000);
return `${hours.toString().padStart(2, "0")}h ${minutes return `${hours.toString().padStart(2, "0")}h ${minutes
.toString() .toString()
.padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`; .padStart(2, "0")}m ${seconds.toString().padStart(2, "0")}s`;
} }
function updateTimer() { function updateTimer() {
timeUntilNext = calculateTimeUntilMidnight(); timeUntilNext = calculateTimeUntilMidnight();
} }
onMount(() => { onMount(() => {
updateTimer(); updateTimer();
intervalId = window.setInterval(updateTimer, 1000); intervalId = window.setInterval(updateTimer, 1000);
}); });
onDestroy(() => { onDestroy(() => {
if (intervalId) { if (intervalId) {
clearInterval(intervalId); clearInterval(intervalId);
} }
}); });
</script> </script>
<div class="text-center py-12"> <div class="w-full">
<div <div
class="inline-flex flex-col items-center bg-white/50 backdrop-blur-sm px-8 py-4 rounded-2xl border border-white/50 shadow-sm" class="flex flex-col items-center bg-white/50 backdrop-blur-sm px-8 py-4 rounded-2xl border border-white/50 shadow-sm w-full"
> >
<p <p class="text-xs uppercase tracking-[0.2em] text-gray-500 font-bold mb-2">
class="text-xs uppercase tracking-[0.2em] text-gray-500 font-bold mb-2" Next Verse In
> </p>
Next Verse In <p class="text-4xl font-triodion font-black text-gray-800 tabular-nums">
</p> {timeUntilNext}
<p class="text-4xl font-triodion font-black text-gray-800 tabular-nums"> </p>
{timeUntilNext} </div>
</p>
</div>
</div> </div>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
</script> </script>
<!-- <div <!-- <div
@@ -14,15 +14,15 @@
> >
</div> --> </div> -->
<div class="text-center py-12"> <div class="text-center" in:fade={{ delay: 1500, duration: 1000 }}>
<div <div
class="inline-flex w-full flex-col items-center bg-white/50 backdrop-blur-sm px-8 py-4 rounded-2xl border border-white/50 shadow-sm" class="inline-flex w-full flex-col items-center bg-white/50 backdrop-blur-sm px-8 py-4 rounded-2xl border border-white/50 shadow-sm"
> >
<p class="text-xs uppercase tracking-[0.2em] text-gray-500 font-bold"> <p class="text-xs uppercase tracking-[0.2em] text-gray-500 font-bold">
A project by George Powell & Silent Summit Co. A project by George Powell & Silent Summit Co.
</p> </p>
<!-- <p class="text-4xl font-triodion font-black text-gray-800 tabular-nums"> <!-- <p class="text-4xl font-triodion font-black text-gray-800 tabular-nums">
</p> --> </p> -->
</div> </div>
</div> </div>

View File

@@ -1,55 +1,53 @@
<script lang="ts"> <script lang="ts">
import { bibleBooks, type BibleBook } from "$lib/types/bible"; import { bibleBooks, type BibleBook } from "$lib/types/bible";
let { searchQuery = $bindable(""), guessedIds, submitGuess } = $props(); let { searchQuery = $bindable(""), guessedIds, submitGuess } = $props();
let filteredBooks = $derived( let filteredBooks = $derived(
bibleBooks.filter((book) => bibleBooks.filter((book) =>
book.name.toLowerCase().includes(searchQuery.toLowerCase()), book.name.toLowerCase().includes(searchQuery.toLowerCase())
), )
); );
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key === "Enter" && filteredBooks.length > 0) { if (e.key === "Enter" && filteredBooks.length > 0) {
submitGuess(filteredBooks[0].id); submitGuess(filteredBooks[0].id);
} }
} }
</script> </script>
<div class="mb-12"> <div>
<input <input
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Type to guess a book (e.g. 'Genesis', 'John')..." 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" 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} onkeydown={handleKeydown}
/> />
{#if searchQuery && filteredBooks.length > 0} {#if searchQuery && filteredBooks.length > 0}
<ul <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-200 rounded-2xl shadow-lg"
> >
{#each filteredBooks as book (book.id)} {#each filteredBooks as book (book.id)}
<li> <li>
<button <button
class="w-full p-4 sm:p-5 text-left {guessedIds.has( class="w-full p-4 sm:p-5 text-left {guessedIds.has(book.id)
book.id, ? 'opacity-60 cursor-not-allowed pointer-events-none hover:bg-gray-100 hover:text-gray-600'
) : 'hover:bg-blue-50 hover:text-blue-700'} transition-all border-b border-gray-100 last:border-b-0 flex items-center"
? 'opacity-60 cursor-not-allowed pointer-events-none hover:bg-gray-100 hover:text-gray-600' onclick={() => submitGuess(book.id)}
: 'hover:bg-blue-50 hover:text-blue-700'} transition-all border-b border-gray-100 last:border-b-0 flex items-center" >
onclick={() => submitGuess(book.id)} <span
> class="font-semibold {guessedIds.has(book.id)
<span ? 'line-through text-gray-500'
class="font-semibold {guessedIds.has(book.id) : ''}">{book.name}</span
? 'line-through text-gray-500' >
: ''}">{book.name}</span <span class="ml-auto text-sm opacity-75"
> >({book.testament.toUpperCase()})</span
<span class="ml-auto text-sm opacity-75" >
>({book.testament.toUpperCase()})</span </button>
> </li>
</button> {/each}
</li> </ul>
{/each} {:else if searchQuery}
</ul> <p class="mt-4 text-center text-gray-500 p-8">No books found</p>
{:else if searchQuery} {/if}
<p class="mt-4 text-center text-gray-500 p-8">No books found</p>
{/if}
</div> </div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <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
import Container from "./Container.svelte";
let { data, isWon }: { data: PageData; isWon: boolean } = $props(); let { data, isWon }: { data: PageData; isWon: boolean } = $props();
let dailyVerse = $derived(data.dailyVerse); let dailyVerse = $derived(data.dailyVerse);
@@ -11,15 +12,17 @@
); );
</script> </script>
<div class="bg-gray-50 rounded-2xl shadow-xl p-8 sm:p-12 mb-4 sm:mb-12 w-full"> <Container class="w-full p-8 sm:p-12">
<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"
> >
{displayVerseText} {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 bg-white/70 rounded-xl px-4 py-2"
>
{displayReference} {displayReference}
</p> </p>
{/if} {/if}
</div> </Container>

View File

@@ -2,6 +2,8 @@
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { getBookById, toOrdinal, getNextGradeMessage } from "$lib/utils/game"; import { getBookById, toOrdinal, getNextGradeMessage } from "$lib/utils/game";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Container from "./Container.svelte";
import CountdownTimer from "./CountdownTimer.svelte";
interface StatsData { interface StatsData {
solveRank: number; solveRank: number;
@@ -36,7 +38,7 @@
const congratulationsMessages: WeightedMessage[] = [ const congratulationsMessages: WeightedMessage[] = [
{ text: "Congratulations!", weight: 10 }, { text: "Congratulations!", weight: 10 },
{ text: "You got it!", weight: 1000 }, { text: "You got it!", weight: 1000 },
{ text: "Yup,", weight: 100 }, { text: "Yup.", weight: 100 },
{ text: "Very nice!", weight: 1 }, { text: "Very nice!", weight: 1 },
]; ];
@@ -73,75 +75,81 @@
let congratulationsMessage = $derived(getRandomCongratulationsMessage()); let congratulationsMessage = $derived(getRandomCongratulationsMessage());
</script> </script>
<div <div class="flex flex-col gap-6">
class="p-8 sm:p-12 w-full bg-linear-to-r from-green-400 to-green-600 text-white rounded-2xl shadow-2xl text-center fade-in" <Container
> class="w-full p-8 sm:p-12 bg-linear-to-r from-green-400/10 to-green-600/30 text-gray-800 shadow-2xl text-center fade-in"
<!-- <h2 class="text-2xl sm:text-4xl font-black mb-4 drop-shadow-lg">
{congratulationsMessage}
</h2> -->
<p class="text-xl sm:text-3xl md:text-4xl">
{congratulationsMessage} The verse is from
<span class="font-black text-xl sm:text-2xl md:text-3xl">{bookName}</span>.
</p>
<p
class="text-2xl font-bold mt-6 p-2 mx-2 bg-black/20 rounded-lg inline-block"
> >
Your grade: {grade} <p class="text-2xl sm:text-3xl md:text-4xl leading-relaxed">
</p> {congratulationsMessage} The verse is from
<span class="font-black text-3xl md:text-4xl">{bookName}</span>.
</p>
<p class="text-lg sm:text-xl md:text-2xl mt-4">
You guessed correctly after {guessCount}
{guessCount === 1 ? "guess" : "guesses"}.
<span class="font-bold bg-white/40 rounded px-1.5 py-0.75">{grade}</span>
</p>
{#if hasWebShare} <div class="flex justify-center mt-6">
<button {#if hasWebShare}
onclick={handleShare} <!-- mobile and arc in production -->
data-umami-event="Share" <button
class="mt-4 text-2xl font-bold p-2 bg-white/20 hover:bg-white/30 rounded-lg inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none" onclick={handleShare}
> data-umami-event="Share"
📤 Share class="text-2xl font-bold p-4 bg-white/70 hover:bg-white/80 rounded-xl inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none"
</button> >
<button 📤 Share
onclick={() => { </button>
copyToClipboard(); <button
copySuccess = true; onclick={() => {
setTimeout(() => { copyToClipboard();
copySuccess = false; copySuccess = true;
}, 3000); setTimeout(() => {
}} copySuccess = false;
data-umami-event="Copy to Clipboard" }, 3000);
class={`mt-4 text-2xl font-bold p-2 rounded-lg inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none ${ }}
copySuccess data-umami-event="Copy to Clipboard"
? "bg-green-400/50 hover:bg-green-500/60" class={`text-2xl font-bold p-4 rounded-lg inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none ${
: "bg-white/20 hover:bg-white/30" copySuccess ? "bg-white/30" : "bg-white/70 hover:bg-white/80"
}`} }`}
> >
{copySuccess ? "✅ Copied!" : "📋 Copy to clipboard"} {copySuccess ? "✅ Copied!" : "📋 Copy"}
</button> </button>
{:else} {:else}
<button <!-- dev mode and desktop browsers -->
onclick={handleShare} <button
data-umami-event="Share" onclick={handleShare}
class={`mt-4 text-2xl font-bold p-2 ${ data-umami-event="Copy to Clipboard"
copied class={`text-2xl font-bold p-4 ${
? "bg-green-400/50 hover:bg-green-500/60" copied ? "bg-white/30" : "bg-white/70 hover:bg-white/80"
: "bg-white/20 hover:bg-white/30" } rounded-xl inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none`}
} rounded-lg inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none`} >
> {copied ? "✅ Copied!" : "📋 Share"}
{copied ? "Copied to clipboard!" : "📤 Share"} </button>
</button> {/if}
{/if} </div>
<p class="pt-6 big-text text-gray-100!"> <p class="pt-6 big-text text-gray-700!">
{getNextGradeMessage(guessCount)} {getNextGradeMessage(guessCount)}
</p> </p>
</Container>
<CountdownTimer />
<!-- Statistics Display --> <!-- Statistics Display -->
{#if statsData} {#if statsData}
<div class="mt-6" in:fade={{ delay: 800 }}> <Container
<div class="grid grid-cols-3 gap-4 gap-x-8 text-center"> class="w-full p-4 bg-white/50 backdrop-blur-sm text-gray-800 shadow-lg text-center"
>
<div
class="grid grid-cols-3 gap-4 gap-x-8 text-center"
in:fade={{ delay: 800 }}
>
<!-- Solve Rank Column --> <!-- Solve Rank Column -->
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black"> <div class="text-3xl sm:text-4xl font-black">
#{statsData.solveRank} #{statsData.solveRank}
</div> </div>
<div class="text-xs sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
You were the {toOrdinal(statsData.solveRank)} person to solve today You were the {toOrdinal(statsData.solveRank)} person to solve today
</div> </div>
</div> </div>
@@ -155,7 +163,7 @@
100 100
)}% )}%
</div> </div>
<div class="text-xs sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
You ranked {toOrdinal(statsData.guessRank)} of {statsData.totalSolves} You ranked {toOrdinal(statsData.guessRank)} of {statsData.totalSolves}
total solves total solves
</div> </div>
@@ -166,15 +174,19 @@
<div class="text-3xl sm:text-4xl font-black"> <div class="text-3xl sm:text-4xl font-black">
{statsData.averageGuesses} {statsData.averageGuesses}
</div> </div>
<div class="text-xs sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
People guessed correctly after {statsData.averageGuesses} People guessed correctly after {statsData.averageGuesses}
{statsData.averageGuesses === 1 ? "guess" : "guesses"} on average {statsData.averageGuesses === 1 ? "guess" : "guesses"} on average
</div> </div>
</div> </div>
</div> </div>
</div> </Container>
{:else if !statsSubmitted} {:else if !statsSubmitted}
<div class="mt-6 text-sm opacity-80">Submitting stats...</div> <Container
class="w-full p-6 bg-white/50 backdrop-blur-sm text-gray-800 shadow-lg text-center"
>
<div class="text-sm opacity-80">Submitting stats...</div>
</Container>
{/if} {/if}
</div> </div>

View File

@@ -7,7 +7,6 @@
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 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";
@@ -408,28 +407,30 @@
> >
</div> </div>
<VerseDisplay {data} {isWon} /> <div class="flex flex-col gap-6">
<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 /> {/if}
{/if}
<GuessesTable {guesses} {correctBookId} /> <GuessesTable {guesses} {correctBookId} />
{#if isWon}
<Feedback /> {#if isWon}
{/if} <Feedback />
{/if}
</div>
{#if isDev} {#if isDev}
<button <button
onclick={clearLocalStorage} onclick={clearLocalStorage}

View File

@@ -1,6 +1,9 @@
# in progress # in progress
- root menu: classic / imposter mode / impossible mode (complete today's classic and imposter modes to unlock) - Show new/old testament after 3 guesses and section after 7 guesses
- Verses ending in semicolons, commas, etc. will be replaced with "..."
- For bonus points: guess the verse/psalm number
- How do you balance rewarding knowledge vs incentivising learning?
# todo # todo
@@ -14,8 +17,12 @@
- classic mode: identify what book the verse is from (e.g. Genesis, John, Revelations...) in as few guesses as possible. - 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 - imposter mode: out of four options, identify the verse that is not in the Bible
- OR out of four options, identify the verse that is not in the same book as the other three options
- OR, out of four options, drag them into the
- impossible mode: identify which book of the bible the verse is from in less than three guesses. - impossible mode: identify which book of the bible the verse is from in less than three guesses.
- The gambling aspect of hoping you get a verse you already know is VERY strong
- add login + saved stats + streak etc. - add login + saved stats + streak etc.
- add deuterocanonical books - add deuterocanonical books