mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-04-06 01:43:32 -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:
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
@@ -60,4 +79,4 @@ html, body {
|
||||
|
||||
.animate-delay-800 {
|
||||
animation-delay: 0.8s;
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user