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,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>
|
||||
|
||||
Reference in New Issue
Block a user