Save theme in cookies (avoid flickering)

This commit is contained in:
Santiago Lo Coco 2024-04-23 22:52:33 +02:00
parent 970d6e2b48
commit 13832dc13a
10 changed files with 211 additions and 3 deletions

6
src/app.d.ts vendored
View File

@ -4,6 +4,12 @@ declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
theme: Theme
}
interface PageData {
theme: Theme
}
// interface PageData {}
// interface PageState {}
// interface Platform {}

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" data-theme="%THEME%">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />

24
src/hooks.server.ts Normal file
View File

@ -0,0 +1,24 @@
import type { Handle } from "@sveltejs/kit"
export type Theme = "light" | "dark" | "auto"
export const isValidTheme = (
theme: FormDataEntryValue | null
): theme is Theme =>
!!theme && (theme === "light" || theme === "dark" || theme === "auto")
export const handle: Handle = async ({ event, resolve }) => {
const theme = event.cookies.get("theme") ?? "auto"
if (isValidTheme(theme)) {
event.locals.theme = theme
}
event.setHeaders({
"cache-control": `private, max-age=${5 * 60}`,
})
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace("%THEME%", theme),
})
return response
}

View File

@ -0,0 +1,74 @@
<script lang="ts" context="module">
export const themes = ["light", "dark", "auto"] as const
// export type Theme = (typeof themes)[number]
</script>
<script lang="ts">
// import {browser} from '$app/environment'
import { applyAction, enhance } from "$app/forms"
import { page } from "$app/stores"
import { slide } from "svelte/transition"
import type { Theme } from "../../hooks.server"
let { theme } = $props<{ theme: Theme }>()
// const deriveNextTheme = (theme: Theme): Theme => {
// switch (theme) {
// case 'dark':
// return 'light'
// case 'light':
// return 'dark'
// case 'auto':
// default:
// if (!browser) return 'auto'
// return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark'
// }
// }
// $derived nextTheme = deriveNextTheme($theme)
function toggleTheme() {
const currentIndex = themes.indexOf(theme)
theme = themes[(currentIndex + 1) % themes.length]
if (theme == "auto") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "light"
: "dark"
}
console.log(theme)
}
let icon = $derived.by(() => {
if (theme === "light") return "🌞"
if (theme === "dark") return "🌙"
if (theme === "auto")
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "🌙"
: "🌞"
})
</script>
<header class="flex items-center justify-between px-2 py-4">
<div class="flex items-center">
<a href="/" class="px-4 text-xl font-semibold">BreakOften</a>
</div>
<nav class="flex items-center gap-4">
<button class="py-2 px-4"> Login </button>
<form
method="POST"
action="/?/theme"
use:enhance={async () => {
return async ({ result }) => {
await applyAction(result)
}
}}
>
<input name="theme" value={theme} hidden />
{#key theme}
<button transition:slide={{ axis: "x" }} onclick={toggleTheme}>
{icon}
</button>
{/key}
</form>
</nav>
</header>

View File

@ -10,6 +10,9 @@
function toggleTheme() {
const currentIndex = themes.indexOf(theme)
theme = themes[(currentIndex + 1) % themes.length]
// if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
// localStorage.setItem("theme", theme)
// }
}
let icon = $derived.by(() => {

View File

View File

@ -0,0 +1,7 @@
import type { LayoutServerLoad } from "./$types"
export const load: LayoutServerLoad = async ({ locals }) => {
const { theme } = locals
return { theme }
}

View File

@ -1,14 +1,22 @@
<script lang="ts">
import NavBar from "$lib/components/NavBar.svelte"
import ThemePicker, { type Theme } from "$lib/components/ThemePicker.svelte"
import "../app.css"
// let lastTheme: string | null = null
// if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
// lastTheme = localStorage.getItem("theme") || "dark"
// }
let { data } = $props()
let theme = $state("dark" as Theme)
let theme = $state(data.theme as Theme)
</script>
<div data-theme={theme}>
<ThemePicker bind:theme />
<!-- <ThemePicker bind:theme /> -->
<NavBar bind:theme />
<slot />
</div>

View File

@ -0,0 +1,19 @@
import { fail, type Actions } from "@sveltejs/kit"
import { isValidTheme } from "../hooks.server"
const TEN_YEARS_IN_SECONDS = 10 * 365 * 24 * 60 * 60
export const actions: Actions = {
theme: async ({ cookies, request }) => {
const data = await request.formData()
const theme = data.get("theme")
if (!isValidTheme(theme)) {
return fail(400, { theme, missing: true })
}
cookies.set("theme", theme, { path: "/", maxAge: TEN_YEARS_IN_SECONDS })
return { success: true }
},
}

67
src/service-worker.js Normal file
View File

@ -0,0 +1,67 @@
/// <reference types="@sveltejs/kit" />
import { build, files, version } from "$service-worker"
const CACHE = `cache-${version}`
const ASSETS = [...build, ...files]
self.addEventListener("install", (event) => {
async function addFilesToCache() {
const cache = await caches.open(CACHE)
await cache.addAll(ASSETS)
}
event.waitUntil(addFilesToCache())
})
self.addEventListener("activate", (event) => {
async function deleteOldCaches() {
for (const key of await caches.keys()) {
if (key !== CACHE) await caches.delete(key)
}
}
event.waitUntil(deleteOldCaches())
})
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return
async function respond() {
const url = new URL(event.request.url)
const cache = await caches.open(CACHE)
if (ASSETS.includes(url.pathname)) {
return cache.match(event.request)
}
try {
const response = await fetch(event.request)
if (response.status === 200) {
cache.put(event.request, response.clone())
}
return response
} catch {
return cache.match(event.request)
}
}
event.respondWith(respond())
})
// self.addEventListener('storage', event => {
// if (event.key === 'theme') {
// const newCacheName = event.newValue === 'dark' ? 'my-cache-dark' : 'my-cache';
// caches.keys().then(cacheNames => {
// return Promise.all(
// cacheNames.filter(cacheName => cacheName === CACHE)
// .map(cacheName => caches.delete(cacheName))
// );
// }).then(() => {
// return caches.open(newCacheName);
// })
// }
// })