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:
George Powell
2026-03-12 18:22:59 -04:00
parent 3de55ba216
commit 884bbe65c7
16 changed files with 444 additions and 94 deletions

View File

@@ -1,26 +1,35 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import "./layout.css";
import favicon from "$lib/assets/favicon.ico";
import { onMount } from '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);
}
});
import "./layout.css";
import favicon from "$lib/assets/favicon.ico";
import TitleAnimation from "$lib/components/TitleAnimation.svelte";
let { children } = $props();
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();
</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>

View File

@@ -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">

View 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)
};
}

View 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>

View File

@@ -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 {
@@ -60,4 +79,4 @@ html, body {
.animate-delay-800 {
animation-delay: 0.8s;
}
}

View 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'
}
});
};