mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-05 17:33:31 -04:00
feat: add about page, sitemap, social links component, Apple sign-in
prompt on win screen, and layout/theme improvements
## New features
- **About page** (`src/routes/about/`): New static about page rendered
from `static/about.md` using the `marked` library (added as a
dependency). Includes the project backstory content.
- **XML sitemap** (`src/routes/sitemap.xml/`): Dynamic sitemap
endpoint for SEO, registered in `static/robots.txt` via `Sitemap:`
directive.
- **Apple Sign In prompt on win screen** (`WinScreen.svelte`): When
the game is won and the user is not logged in, a "Sign in to save
your streak & see your stats" prompt with an Apple Sign In button is
shown below the share card. Passes `anonymousId` so stats migrate on
sign-up. Driven by new `isLoggedIn` and `anonymousId` props, passed
from `+page.svelte`.
## Refactoring
- **`SocialLinks` component**
(`src/lib/components/SocialLinks.svelte`): Extracted the Bluesky,
Twitter/X, and email social link icons from `Credits.svelte` into a
reusable component. `Credits.svelte` now imports and renders
`<SocialLinks />`.
- **`ThemeToggle` component**
(`src/lib/components/ThemeToggle.svelte`): New component for
toggling light/dark mode, persisted to `localStorage`. Currently
rendered but hidden (`hidden` class) in `+page.svelte` —
infrastructure is in place for future use.
## Layout changes
- **`+layout.svelte`**: Moved the page title/header (`<h1>` with
`TitleAnimation`) and the gradient background wrapper from
`+page.svelte` into the root layout, so it applies across all
routes. Also removed the `browser` guard around the analytics script
injection (it's
already inside `onMount` which is client-only). Added `<meta
name="description">`.
- **`+page.svelte`**: Removed the title/header and gradient wrapper
(now in layout). Minor formatting cleanup (reformatted `SearchInput`
props, moved `currentDate` derived state earlier). `ThemeToggle`
import swapped in place of `TitleAnimation` (which moved to layout).
## Styling
- **`layout.css`**: Added `@custom-variant dark` for class-based dark
mode toggling (supports `.dark` class on `<html>`). Added explicit
`html.dark` / `html.light` rules alongside the existing
`prefers-color-scheme` media query, so the `ThemeToggle` component
can
override the system preference. Added background transition
animation.
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -7,6 +7,7 @@
|
||||
"dependencies": {
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"fast-xml-parser": "^5.3.3",
|
||||
"marked": "^17.0.4",
|
||||
"xml2js": "^0.6.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -394,6 +395,8 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"marked": ["marked@17.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ=="],
|
||||
|
||||
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"dependencies": {
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"fast-xml-parser": "^5.3.3",
|
||||
"marked": "^17.0.4",
|
||||
"xml2js": "^0.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import BlueskyLogo from "$lib/assets/Bluesky_Logo.svg";
|
||||
import TwitterLogo from "$lib/assets/Twitter_Logo.svg";
|
||||
import SocialLinks from "$lib/components/SocialLinks.svelte";
|
||||
</script>
|
||||
|
||||
<div class="text-center" in:fade={{ delay: 1500, duration: 1000 }}>
|
||||
@@ -28,56 +27,8 @@
|
||||
<!-- Bluesky Social Media Button -->
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex items-center justify-center gap-6">
|
||||
<a
|
||||
href="https://bsky.app/profile/snail.city"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex hover:opacity-80 transition-opacity"
|
||||
aria-label="Follow on Bluesky"
|
||||
data-umami-event="Bluesky clicked"
|
||||
onclick={() => (window as any).rybbit?.event("Bluesky clicked")}
|
||||
>
|
||||
<img src={BlueskyLogo} alt="Bluesky" class="w-8 h-8" />
|
||||
</a>
|
||||
|
||||
<div class="w-0.5 h-8 bg-gray-400 dark:bg-gray-600"></div>
|
||||
|
||||
<a
|
||||
href="https://x.com/pupperpowell"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex hover:opacity-80 transition-opacity"
|
||||
aria-label="Follow on Twitter"
|
||||
data-umami-event="Twitter clicked"
|
||||
onclick={() => (window as any).rybbit?.event("Twitter clicked")}
|
||||
>
|
||||
<img src={TwitterLogo} alt="Twitter" class="w-8 h-8" />
|
||||
</a>
|
||||
|
||||
<div class="w-0.5 h-8 bg-gray-400 dark:bg-gray-600"></div>
|
||||
|
||||
<a
|
||||
href="mailto:george+bibdle@silentsummit.co"
|
||||
class="inline-flex hover:opacity-80 transition-opacity"
|
||||
aria-label="Send email"
|
||||
data-umami-event="Email clicked"
|
||||
onclick={() => (window as any).rybbit?.event("Email clicked")}
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-gray-700 dark:text-gray-300"
|
||||
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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="mt-8">
|
||||
<SocialLinks />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
57
src/lib/components/SocialLinks.svelte
Normal file
57
src/lib/components/SocialLinks.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import BlueskyLogo from "$lib/assets/Bluesky_Logo.svg";
|
||||
import TwitterLogo from "$lib/assets/Twitter_Logo.svg";
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center gap-6">
|
||||
<a
|
||||
href="https://bsky.app/profile/snail.city"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex hover:opacity-80 transition-opacity"
|
||||
aria-label="Follow on Bluesky"
|
||||
data-umami-event="Bluesky clicked"
|
||||
onclick={() => (window as any).rybbit?.event("Bluesky clicked")}
|
||||
>
|
||||
<img src={BlueskyLogo} alt="Bluesky" class="w-8 h-8" />
|
||||
</a>
|
||||
|
||||
<div class="w-0.5 h-8 bg-gray-400 dark:bg-gray-600"></div>
|
||||
|
||||
<a
|
||||
href="https://x.com/pupperpowell"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex hover:opacity-80 transition-opacity"
|
||||
aria-label="Follow on Twitter"
|
||||
data-umami-event="Twitter clicked"
|
||||
onclick={() => (window as any).rybbit?.event("Twitter clicked")}
|
||||
>
|
||||
<img src={TwitterLogo} alt="Twitter" class="w-8 h-8" />
|
||||
</a>
|
||||
|
||||
<div class="w-0.5 h-8 bg-gray-400 dark:bg-gray-600"></div>
|
||||
|
||||
<a
|
||||
href="mailto:george+bibdle@silentsummit.co"
|
||||
class="inline-flex hover:opacity-80 transition-opacity"
|
||||
aria-label="Send email"
|
||||
data-umami-event="Email clicked"
|
||||
onclick={() => (window as any).rybbit?.event("Email clicked")}
|
||||
>
|
||||
<svg
|
||||
class="w-8 h-8 text-gray-700 dark:text-gray-300"
|
||||
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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
62
src/lib/components/ThemeToggle.svelte
Normal file
62
src/lib/components/ThemeToggle.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let isDarkMode = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
const stored = localStorage.getItem('bibdle-theme');
|
||||
if (stored === 'dark') {
|
||||
isDarkMode = true;
|
||||
} else if (stored === 'light') {
|
||||
isDarkMode = false;
|
||||
} else {
|
||||
isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.remove('light');
|
||||
localStorage.setItem('bibdle-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.classList.add('light');
|
||||
localStorage.setItem('bibdle-theme', 'light');
|
||||
}
|
||||
});
|
||||
|
||||
function toggleTheme() {
|
||||
isDarkMode = !isDarkMode;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if browser}
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||
class="flex items-center gap-2 p-1 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 transition-colors duration-200 cursor-pointer"
|
||||
>
|
||||
{#if isDarkMode}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2"/>
|
||||
<path d="M12 20v2"/>
|
||||
<path d="m4.93 4.93 1.41 1.41"/>
|
||||
<path d="m17.66 17.66 1.41 1.41"/>
|
||||
<path d="M2 12h2"/>
|
||||
<path d="M20 12h2"/>
|
||||
<path d="m6.34 17.66-1.41 1.41"/>
|
||||
<path d="m19.07 4.93-1.41 1.41"/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>
|
||||
</svg>
|
||||
{/if}
|
||||
<span class="text-xs uppercase tracking-widest">
|
||||
{isDarkMode ? 'Light mode' : 'Dark mode'}
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -39,6 +39,8 @@
|
||||
verseText,
|
||||
streak = 0,
|
||||
streakPercentile = null,
|
||||
isLoggedIn = false,
|
||||
anonymousId = '',
|
||||
}: {
|
||||
statsData: StatsData | null;
|
||||
correctBookId: string;
|
||||
@@ -53,6 +55,8 @@
|
||||
verseText: string;
|
||||
streak?: number;
|
||||
streakPercentile?: number | null;
|
||||
isLoggedIn?: boolean;
|
||||
anonymousId?: string;
|
||||
} = $props();
|
||||
|
||||
let bookName = $derived(getBookById(correctBookId)?.name ?? "");
|
||||
@@ -319,6 +323,24 @@
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isLoggedIn}
|
||||
<div class="signin-prompt">
|
||||
<p class="signin-text">Sign in to save your streak & see your stats</p>
|
||||
<form method="POST" action="/auth/apple">
|
||||
<input type="hidden" name="anonymousId" value={anonymousId} />
|
||||
<button
|
||||
type="submit"
|
||||
class="apple-signin-btn"
|
||||
>
|
||||
<svg class="apple-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
|
||||
</svg>
|
||||
Sign in with Apple
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -598,4 +620,71 @@
|
||||
.snippet-toggle.on .toggle-thumb {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* ── Apple Sign In prompt ── */
|
||||
.signin-prompt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem 0 0.25rem;
|
||||
}
|
||||
|
||||
.signin-text {
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.signin-text {
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.apple-signin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 1.5rem;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease, transform 80ms ease;
|
||||
}
|
||||
|
||||
.apple-signin-btn:hover {
|
||||
background: #222;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.apple-signin-btn:active {
|
||||
background: #111;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.apple-signin-btn {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
.apple-signin-btn:hover {
|
||||
background: #e5e5e5;
|
||||
}
|
||||
.apple-signin-btn:active {
|
||||
background: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.apple-icon {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import "./layout.css";
|
||||
import favicon from "$lib/assets/favicon.ico";
|
||||
import "./layout.css";
|
||||
import favicon from "$lib/assets/favicon.ico";
|
||||
import TitleAnimation from "$lib/components/TitleAnimation.svelte";
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const script = document.createElement('script');
|
||||
script.defer = true;
|
||||
script.src = 'https://umami.snail.city/script.js';
|
||||
script.setAttribute('data-website-id', '5b8c31ad-71cd-4317-940b-6bccea732acc');
|
||||
script.setAttribute('data-domains', 'bibdle.com,www.bibdle.com');
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
// Inject analytics script
|
||||
const script = document.createElement('script');
|
||||
script.defer = true;
|
||||
script.src = 'https://umami.snail.city/script.js';
|
||||
script.setAttribute('data-website-id', '5b8c31ad-71cd-4317-940b-6bccea732acc');
|
||||
script.setAttribute('data-domains', 'bibdle.com,www.bibdle.com');
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<link rel="alternate" type="application/rss+xml" title="Bibdle RSS Feed" href="/feed.xml" />
|
||||
<link rel="icon" href={favicon} />
|
||||
<link rel="alternate" type="application/rss+xml" title="Bibdle RSS Feed" href="/feed.xml" />
|
||||
<meta name="description" content="A daily Bible game" />
|
||||
</svelte:head>
|
||||
{@render children()}
|
||||
|
||||
<div class="min-h-dvh md:bg-linear-to-br md:from-blue-50 md:to-indigo-200 dark:md:from-gray-900 dark:md:to-slate-950">
|
||||
<h1
|
||||
class="text-3xl md:text-4xl font-bold text-center uppercase text-gray-600 dark:text-gray-300 drop-shadow-2xl tracking-widest p-4 pt-12 animate-fade-in-up"
|
||||
>
|
||||
<TitleAnimation />
|
||||
<div class="font-normal"></div>
|
||||
</h1>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import GuessesTable from "$lib/components/GuessesTable.svelte";
|
||||
import WinScreen from "$lib/components/WinScreen.svelte";
|
||||
import Credits from "$lib/components/Credits.svelte";
|
||||
import TitleAnimation from "$lib/components/TitleAnimation.svelte";
|
||||
import ThemeToggle from "$lib/components/ThemeToggle.svelte";
|
||||
import DevButtons from "$lib/components/DevButtons.svelte";
|
||||
import AuthModal from "$lib/components/AuthModal.svelte";
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
let user = $derived(data.user);
|
||||
let session = $derived(data.session);
|
||||
|
||||
const currentDate = $derived(
|
||||
new Date().toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
);
|
||||
|
||||
let searchQuery = $state("");
|
||||
let copied = $state(false);
|
||||
let isDev = $state(false);
|
||||
@@ -55,15 +64,6 @@
|
||||
new SvelteSet(persistence.guesses.map((g) => g.book.id)),
|
||||
);
|
||||
|
||||
const currentDate = $derived(
|
||||
new Date().toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
);
|
||||
|
||||
let isWon = $derived(
|
||||
persistence.guesses.some((g) => g.book.id === correctBookId),
|
||||
);
|
||||
@@ -283,20 +283,13 @@
|
||||
<title>A daily bible game{isDev ? " (dev)" : ""}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-dvh md:bg-linear-to-br md:from-blue-50 md:to-indigo-200 dark:md:from-gray-900 dark:md:to-slate-950 py-8">
|
||||
<div class="pb-8">
|
||||
<div class="w-full max-w-3xl mx-auto px-4">
|
||||
<h1
|
||||
class="text-3xl md:text-4xl font-bold text-center uppercase text-gray-600 dark:text-gray-300 drop-shadow-2xl tracking-widest p-4 animate-fade-in-up"
|
||||
>
|
||||
<TitleAnimation />
|
||||
<div class="font-normal"></div>
|
||||
</h1>
|
||||
<div class="text-center mb-8 animate-fade-in-up animate-delay-200">
|
||||
<span class="big-text"
|
||||
>{isDev ? "Dev Edition | " : ""}{currentDate}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="animate-fade-in-up animate-delay-200">
|
||||
<VerseDisplay {data} {isWon} {blurChapter} />
|
||||
@@ -304,7 +297,12 @@
|
||||
|
||||
{#if !isWon}
|
||||
<div class="animate-fade-in-up animate-delay-400">
|
||||
<SearchInput bind:searchQuery {guessedIds} {submitGuess} guessCount={persistence.guesses.length} />
|
||||
<SearchInput
|
||||
bind:searchQuery
|
||||
{guessedIds}
|
||||
{submitGuess}
|
||||
guessCount={persistence.guesses.length}
|
||||
/>
|
||||
</div>
|
||||
{:else if showWinScreen}
|
||||
<div class="animate-fade-in-up animate-delay-400">
|
||||
@@ -322,6 +320,8 @@
|
||||
verseText={dailyVerse.verseText}
|
||||
{streak}
|
||||
{streakPercentile}
|
||||
isLoggedIn={!!user}
|
||||
anonymousId={persistence.anonymousId}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -335,6 +335,11 @@
|
||||
<Credits />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- We will just go with the user's system color theme for now. -->
|
||||
<div class="flex justify-center hidden mt-4">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
{#if isDev}
|
||||
<div class="mt-8 flex flex-col items-stretch md:items-center gap-3">
|
||||
|
||||
13
src/routes/about/+page.server.ts
Normal file
13
src/routes/about/+page.server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { marked } from 'marked';
|
||||
|
||||
export async function load() {
|
||||
const about = readFileSync(resolve('static/about.md'), 'utf-8');
|
||||
const howToPlay = readFileSync(resolve('static/how-to-play.md'), 'utf-8');
|
||||
|
||||
return {
|
||||
about: await marked(about),
|
||||
howToPlay: await marked(howToPlay)
|
||||
};
|
||||
}
|
||||
48
src/routes/about/+page.svelte
Normal file
48
src/routes/about/+page.svelte
Normal file
@@ -0,0 +1,48 @@
|
||||
<svelte:head>
|
||||
<title>About — Bibdle</title>
|
||||
</svelte:head>
|
||||
|
||||
<script lang="ts">
|
||||
import SocialLinks from "$lib/components/SocialLinks.svelte";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const SOCIAL_PLACEHOLDER = "<!-- social -->";
|
||||
|
||||
const aboutParts = $derived(
|
||||
data.about.includes(SOCIAL_PLACEHOLDER)
|
||||
? data.about.split(SOCIAL_PLACEHOLDER)
|
||||
: null
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="min-h-dvh py-10 px-4">
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
|
||||
<div class="mb-8">
|
||||
<a
|
||||
href="/"
|
||||
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
← Back to Game
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="prose dark:prose-invert text-justify max-w-none">
|
||||
{#if aboutParts}
|
||||
{@html aboutParts[0]}
|
||||
<div class="my-8 not-prose">
|
||||
<SocialLinks />
|
||||
</div>
|
||||
{@html aboutParts[1]}
|
||||
{:else}
|
||||
{@html data.about}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="prose dark:prose-invert text-justify max-w-none mt-10">
|
||||
{@html data.howToPlay}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,20 +2,31 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin '@tailwindcss/typography';
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--font-triodion: "PT Serif", serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
background: oklch(89.126% 0.06134 298.626);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html, body {
|
||||
html:not(.light), body:not(.light) {
|
||||
background: oklch(18% 0.03 298.626);
|
||||
}
|
||||
}
|
||||
|
||||
html.dark, html.dark body {
|
||||
background: oklch(18% 0.03 298.626);
|
||||
}
|
||||
|
||||
html.light, html.light body {
|
||||
background: oklch(89.126% 0.06134 298.626);
|
||||
}
|
||||
|
||||
.big-text {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
@@ -25,11 +36,19 @@ html, body {
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.big-text {
|
||||
html:not(.light) .big-text {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .big-text {
|
||||
color: rgb(156 163 175);
|
||||
}
|
||||
|
||||
html.light .big-text {
|
||||
color: rgb(107 114 128);
|
||||
}
|
||||
|
||||
/* Page load animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
|
||||
23
src/routes/sitemap.xml/+server.ts
Normal file
23
src/routes/sitemap.xml/+server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = () => {
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://bibdle.com/</loc>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://bibdle.com/about</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.5</priority>
|
||||
</url>
|
||||
</urlset>`;
|
||||
|
||||
return new Response(sitemap, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml'
|
||||
}
|
||||
});
|
||||
};
|
||||
15
static/about.md
Normal file
15
static/about.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# About Bibdle
|
||||
|
||||
Bibdle is a daily Bible guessing game. Every day, a random verse is posted to the website. Try to figure out which book of the Bible it comes from, in as few guesses as possible. That's it!
|
||||
|
||||
---
|
||||
|
||||
The game was built with the hope that it would be a small, delightful thing people can make part of their day. It's not a Bible study course. You don't need to know the Bible inside and out to enjoy it or to learn something from it.
|
||||
|
||||
If you're someone who grew up in church and can name all 66 books in order, great. If you're someone who can barely tell the Old Testament from the New, that's great too. The game meets you where you are.
|
||||
|
||||
It is completely free. If you'd like to support the developer (who works solely on small projects like this one) or express your thanks, you can become a Bibdle patron.
|
||||
|
||||
If you use Bibdle, I would love to hear from you! I can be reached via email or through Bluesky.
|
||||
|
||||
<!-- social -->
|
||||
47
static/how-to-play.md
Normal file
47
static/how-to-play.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# How to Play
|
||||
|
||||
Each day, Bibdle gives you a verse from the Bible. Your job is to guess which book it comes from. (Genesis, John, Corinthians, etc.)
|
||||
|
||||
You have unlimited guesses.
|
||||
|
||||
---
|
||||
|
||||
## The Basics
|
||||
|
||||
1. **Read the verse.** It appears at the top of the page.
|
||||
2. **Make a guess.** Type or select a book of the Bible from the list.
|
||||
3. **Read the feedback.** After each guess, you'll get clues telling you how close you were.
|
||||
4. **Keep guessing** until you get it right.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Feedback Hints
|
||||
|
||||
After each wrong guess, you'll see the following hints:
|
||||
|
||||
| Hint | What it means |
|
||||
|---|---|
|
||||
| **Testament** | If your guess was in the correct Testament (Old or New) |
|
||||
| **Section** | If your guess was in the correct section of the Bible (e.g. Gospels, Epistles, Major Prophets) |
|
||||
| **First Letter** | If your guess has the same first letter as the correct guess |
|
||||
|
||||
Use the hints to narrow down your search.
|
||||
|
||||
---
|
||||
|
||||
## A Few Things to Know
|
||||
|
||||
- **Everyone plays the same verse each day.** The daily verse resets at midnight.
|
||||
- **Your progress is saved automatically.** You can close the tab and come back later.
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- Pay attention to writing style: the voice of Psalms is very different from Paul's letters.
|
||||
- Historical narrative (battles, kings, genealogies) tends to be Old Testament.
|
||||
- Short, poetic, or wisdom-focused verses could be Proverbs, Ecclesiastes, or Psalms.
|
||||
- If a verse mentions Jesus by name, it's in the New Testament.
|
||||
|
||||
Good luck!
|
||||
@@ -1,3 +1,5 @@
|
||||
# allow crawling everything by default
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
||||
Sitemap: https://bibdle.com/sitemap.xml
|
||||
|
||||
6
todo.md
6
todo.md
@@ -59,6 +59,12 @@ I created Bibdle from a combination of two things. The first is my lifelong desi
|
||||
|
||||
# done
|
||||
|
||||
## march 12th
|
||||
|
||||
- Added about page with social buttons and XML sitemap for SEO
|
||||
- Fixed incorrect header background color on Desktop
|
||||
- Added color theme toggle button (commented out for now)
|
||||
|
||||
## feb 26th
|
||||
|
||||
- Added dark mode
|
||||
|
||||
Reference in New Issue
Block a user