This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { assets, resolve } from '$app/paths';
|
||||
import { dev } from '$app/environment';
|
||||
import { page } from '$app/state';
|
||||
import { getSeo } from '$lib/seo';
|
||||
import CopyButton from '$lib/components/CopyButton.svelte';
|
||||
import SiOsu from '~icons/simple-icons/osu';
|
||||
import SiSteam from '~icons/simple-icons/steam';
|
||||
import SiTwitch from '~icons/simple-icons/twitch';
|
||||
@@ -10,47 +15,105 @@
|
||||
import SiLinkedin from '~icons/simple-icons/linkedin';
|
||||
import SiInstagram from '~icons/simple-icons/instagram';
|
||||
import SiX from '~icons/simple-icons/x';
|
||||
// import SiBluesky from '~icons/simple-icons/bluesky';
|
||||
// import SiThreads from '~icons/simple-icons/threads';
|
||||
import SiMail from '~icons/simple-icons/gmail';
|
||||
|
||||
import CopyButton from '$lib/components/CopyButton.svelte';
|
||||
import SiDiscord from '~icons/simple-icons/discord';
|
||||
import SiSignal from '~icons/simple-icons/signal';
|
||||
import SiWhatsapp from '~icons/simple-icons/whatsapp';
|
||||
import { resolve } from '$app/paths';
|
||||
import { dev } from '$app/environment';
|
||||
import { getTitle } from '$lib/title';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const links = [
|
||||
{ icon: SiMail, label: 'Email', href: 'mailto:stan@stanrunge.dev' },
|
||||
{ icon: SiGithub, label: 'GitHub', href: 'https://github.com/stanrunge' },
|
||||
{ icon: SiLinkedin, label: 'LinkedIn', href: 'https://linkedin.com/in/stanrunge' },
|
||||
{ icon: SiX, label: 'X', href: 'https://x.com/stanrunge' },
|
||||
{ icon: SiYoutube, label: 'YouTube', href: 'https://youtube.com/@stanrunge' },
|
||||
{ icon: SiInstagram, label: 'Instagram', href: 'https://instagram.com/stan_runge' },
|
||||
// { icon: SiBluesky, label: 'Bluesky', href: 'https://bsky.app/profile/stanrunge.bsky.social' },
|
||||
// { icon: SiThreads, label: 'Threads', href: 'https://www.threads.com/@stan_runge' },
|
||||
{ icon: SiTwitch, label: 'Twitch', href: 'https://twitch.tv/stanrunge_' },
|
||||
let { data, children } = $props();
|
||||
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
|
||||
let menuOpen = $state(false);
|
||||
type ThemePreference = 'dark' | 'system' | 'light';
|
||||
let theme = $state<ThemePreference>('dark');
|
||||
const navigation = [
|
||||
{ label: 'home', href: '/' },
|
||||
{ label: 'projects', href: '/projects' },
|
||||
{ label: 'blog', href: '/blog' },
|
||||
{ label: 'lists', href: '/lists' }
|
||||
] as const;
|
||||
const socialLinks = [
|
||||
{ icon: SiMail, label: 'email', href: 'mailto:stan@stanrunge.dev' },
|
||||
{ icon: SiGithub, label: 'github', href: 'https://github.com/stanrunge' },
|
||||
{ icon: SiLinkedin, label: 'linkedin', href: 'https://linkedin.com/in/stanrunge' },
|
||||
{ icon: SiX, label: 'x / twitter', href: 'https://x.com/stanrunge' },
|
||||
{ icon: SiYoutube, label: 'youtube', href: 'https://youtube.com/@stanrunge' },
|
||||
{ icon: SiInstagram, label: 'instagram', href: 'https://instagram.com/stan_runge' },
|
||||
{ icon: SiTwitch, label: 'twitch', href: 'https://twitch.tv/stanrunge_' },
|
||||
{
|
||||
icon: SiSpotify,
|
||||
label: 'Spotify',
|
||||
label: 'spotify',
|
||||
href: 'https://open.spotify.com/user/e3o3qr9z7fpsu4gur9zz4y1ld'
|
||||
},
|
||||
{ icon: SiSteam, label: 'Steam', href: 'https://steamcommunity.com/id/stanrunge' },
|
||||
{ icon: SiSteam, label: 'steam', href: 'https://steamcommunity.com/id/stanrunge' },
|
||||
{ icon: SiOsu, label: 'osu!', href: 'https://osu.ppy.sh/users/11212255' }
|
||||
];
|
||||
|
||||
const copies = [
|
||||
const copyLinks = [
|
||||
{ icon: SiWhatsapp, label: 'WhatsApp', value: 'stanrunge' },
|
||||
{ icon: SiDiscord, label: 'Discord', value: 'stanrunge' },
|
||||
{ icon: SiSignal, label: 'Signal', value: 'stan.443' }
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
const preference = document.documentElement.dataset.themePreference;
|
||||
theme = preference === 'light' || preference === 'system' ? preference : 'dark';
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: light)');
|
||||
const onSystemThemeChange = () => {
|
||||
if (theme === 'system') applyTheme('system');
|
||||
};
|
||||
systemTheme.addEventListener('change', onSystemThemeChange);
|
||||
return () => systemTheme.removeEventListener('change', onSystemThemeChange);
|
||||
});
|
||||
|
||||
function applyTheme(preference: ThemePreference) {
|
||||
const resolvedTheme =
|
||||
preference === 'system'
|
||||
? window.matchMedia('(prefers-color-scheme: light)').matches
|
||||
? 'light'
|
||||
: 'dark'
|
||||
: preference;
|
||||
document.documentElement.dataset.themePreference = preference;
|
||||
document.documentElement.dataset.theme = resolvedTheme;
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', resolvedTheme === 'light' ? '#ffffff' : '#000000');
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme = theme === 'dark' ? 'system' : theme === 'system' ? 'light' : 'dark';
|
||||
applyTheme(theme);
|
||||
try {
|
||||
localStorage.setItem('theme', theme);
|
||||
} catch {
|
||||
// The selected theme still applies for this visit.
|
||||
}
|
||||
}
|
||||
|
||||
function current(href: string) {
|
||||
return page.url.pathname === href || (href !== '/' && page.url.pathname.startsWith(href + '/'));
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet themeIcon()}
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="22"
|
||||
height="22"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 2v2M12 20v2M4.93 4.93l1.42 1.42m11.3 11.3 1.42 1.42M2 12h2m16 0h2M4.93 19.07l1.42-1.42m11.3-11.3 1.42-1.42"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="6" fill={theme === 'light' ? 'currentColor' : 'none'} />
|
||||
{#if theme === 'system'}
|
||||
<path d="M12 6a6 6 0 0 1 0 12Z" fill="currentColor" stroke="none" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
<svelte:head>
|
||||
{#if !dev}
|
||||
<script
|
||||
@@ -59,45 +122,157 @@
|
||||
data-website-id="47f2b385-188a-4776-a287-4889008ebac5"
|
||||
></script>
|
||||
{/if}
|
||||
|
||||
<title>{getTitle(page.url.pathname)}</title>
|
||||
<title>{seo.title}</title>
|
||||
<meta name="description" content={seo.description} />
|
||||
<meta name="robots" content={seo.indexable ? 'index, follow' : 'noindex, nofollow'} />
|
||||
<link rel="canonical" href={seo.url} />
|
||||
<meta property="og:site_name" content="Stan Runge" />
|
||||
<meta property="og:type" content={seo.type} />
|
||||
<meta property="og:title" content={seo.title} />
|
||||
<meta property="og:description" content={seo.description} />
|
||||
<meta property="og:url" content={seo.url} />
|
||||
<meta property="og:image" content={seo.image} />
|
||||
<meta property="og:image:alt" content={seo.imageAlt} />
|
||||
<meta property="og:image:width" content="793" />
|
||||
<meta property="og:image:height" content="793" />
|
||||
{#if seo.modifiedAt}<meta property="article:modified_time" content={seo.modifiedAt} />{/if}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:creator" content="@stanrunge" />
|
||||
<meta name="twitter:title" content={seo.title} />
|
||||
<meta name="twitter:description" content={seo.description} />
|
||||
<meta name="twitter:image" content={seo.image} />
|
||||
<meta name="twitter:image:alt" content={seo.imageAlt} />
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<header class="flex justify-between items-center gap-4 py-4 px-4 border-b-4 border-gray-300">
|
||||
<a href={resolve('/')} class="text-2xl font-bold underline">Stan Runge</a>
|
||||
<div class="flex gap-8">
|
||||
<div class="text-center py-2 font-bold text-xl ">
|
||||
<a href={resolve('/blog')}>Blog</a>
|
||||
</div>
|
||||
|
||||
<div class="text-center py-2 font-bold text-xl ">
|
||||
<a href={resolve('/projects')}>Projects</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-wrap justify-center gap-4" aria-label="Social links">
|
||||
{#each copies as c (c.label)}
|
||||
<CopyButton {...c} />
|
||||
{/each}
|
||||
{#each links as { icon: Icon, label, href } (label)}
|
||||
<a {href} aria-label={label} class="text-2xl transition hover:opacity-60">
|
||||
<Icon />
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<footer class="flex justify-center py-8 bg-gray-200">
|
||||
<p>
|
||||
Hosted on <a
|
||||
class="underline text-blue-500"
|
||||
href="https://git.stanrunge.dev/stan/personal-website">my git server :)</a
|
||||
<div class="flex min-h-screen flex-col bg-[var(--site-bg)] text-[var(--site-text)]">
|
||||
<a
|
||||
href="#main"
|
||||
class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:bg-[var(--site-bg)] focus:px-4 focus:py-2"
|
||||
>skip to content</a
|
||||
>
|
||||
<header class="border-b border-[var(--site-border)]">
|
||||
<div class="mx-auto flex max-w-6xl items-center gap-5 px-4 py-4 sm:px-6">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class="flex shrink-0 items-center gap-3 text-base font-semibold tracking-tight"
|
||||
onclick={() => (menuOpen = false)}
|
||||
>
|
||||
</p>
|
||||
<span class="h-7 w-7 overflow-hidden rounded-full bg-black">
|
||||
<img
|
||||
src="{assets}/favicon-32x32.png"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
class="h-full w-full scale-[1.7]"
|
||||
/>
|
||||
</span>
|
||||
<span>stan runge</span>
|
||||
</a>
|
||||
<nav class="ml-auto hidden items-center gap-6 text-base md:flex" aria-label="Main navigation">
|
||||
{#each navigation as item (item.href)}
|
||||
<a
|
||||
href={resolve(item.href)}
|
||||
aria-current={current(item.href) ? 'page' : undefined}
|
||||
class={current(item.href)
|
||||
? 'text-[var(--site-text)]'
|
||||
: 'text-[var(--site-muted)] hover:text-[var(--site-text)]'}>{item.label}</a
|
||||
>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="ml-auto hidden items-center gap-5 text-base md:ml-4 md:flex">
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleTheme}
|
||||
aria-label={`Theme: ${theme}. Switch to ${theme === 'dark' ? 'system' : theme === 'system' ? 'light' : 'dark'} mode`}
|
||||
title={`Theme: ${theme}`}
|
||||
class="flex h-10 w-10 items-center justify-center text-[var(--site-muted)] hover:text-[var(--site-text)]"
|
||||
>{@render themeIcon()}</button
|
||||
>
|
||||
{#if data.user}
|
||||
<a href={resolve('/account')} class="max-w-28 truncate hover:underline"
|
||||
>{data.user.name}</a
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(
|
||||
`/login?returnTo=${encodeURIComponent(page.url.pathname + page.url.search)}`
|
||||
)}
|
||||
class="hover:underline">sign in</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto text-base text-[var(--site-muted)] md:hidden"
|
||||
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
onclick={() => (menuOpen = !menuOpen)}>{menuOpen ? 'close' : 'menu'}</button
|
||||
>
|
||||
</div>
|
||||
{#if menuOpen}
|
||||
<nav
|
||||
id="mobile-menu"
|
||||
class="border-t border-[var(--site-border)] px-4 py-4 md:hidden"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
<div class="mx-auto flex max-w-6xl flex-col items-center gap-1 text-center text-base">
|
||||
{#each navigation as item (item.href)}
|
||||
<a href={resolve(item.href)} onclick={() => (menuOpen = false)} class="w-full py-2"
|
||||
>{item.label}</a
|
||||
>
|
||||
{/each}
|
||||
<div
|
||||
class="mt-3 flex w-full items-center justify-center gap-5 border-t border-[var(--site-border)] pt-4"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleTheme}
|
||||
aria-label={`Theme: ${theme}. Switch to ${theme === 'dark' ? 'system' : theme === 'system' ? 'light' : 'dark'} mode`}
|
||||
title={`Theme: ${theme}`}
|
||||
class="flex h-10 w-10 items-center justify-center text-[var(--site-muted)]"
|
||||
>{@render themeIcon()}</button
|
||||
>
|
||||
{#if data.user}
|
||||
<a href={resolve('/account')} onclick={() => (menuOpen = false)}>account</a>
|
||||
<form method="POST" action="/logout"><button>sign out</button></form>
|
||||
{:else}
|
||||
<a href={resolve('/login')} onclick={() => (menuOpen = false)}>sign in</a>
|
||||
<a href={resolve('/register')} onclick={() => (menuOpen = false)}>register</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
</header>
|
||||
<main id="main" class="min-w-0 flex-1">{@render children()}</main>
|
||||
<footer class="mt-16 border-t border-[var(--site-border)]">
|
||||
<div class="mx-auto max-w-6xl space-y-5 px-4 py-8 text-[var(--site-muted)] sm:px-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 text-base">
|
||||
<span
|
||||
>stan runge <span class="ml-2 font-mono text-xs" title="Deployed commit"
|
||||
>{data.version.slice(0, 7)}</span
|
||||
></span
|
||||
>
|
||||
<a
|
||||
href="https://git.stanrunge.dev/stan/personal-website"
|
||||
rel="external"
|
||||
class="hover:text-[var(--site-text)]">source ↗</a
|
||||
>
|
||||
</div>
|
||||
<nav class="flex flex-wrap items-center justify-center gap-1" aria-label="Social links">
|
||||
{#each socialLinks as { icon: Icon, label, href } (label)}
|
||||
<a
|
||||
{href}
|
||||
rel="external"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
class="flex h-10 w-10 items-center justify-center text-xl hover:text-[var(--site-text)]"
|
||||
><Icon /></a
|
||||
>
|
||||
{/each}
|
||||
{#each copyLinks as item (item.label)}<CopyButton {...item} />{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user