Theme persistance in cookie

This commit is contained in:
Evert Prants 2024-06-08 20:22:39 +03:00
parent 3b16762f0e
commit 8dd6ccc58c
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
9 changed files with 104 additions and 33 deletions

View File

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

View File

@ -3,6 +3,8 @@ import { csrf } from '$lib/server/csrf';
import { DB } from '$lib/server/drizzle'; import { DB } from '$lib/server/drizzle';
import { runSeeds } from '$lib/server/drizzle/seeds'; import { runSeeds } from '$lib/server/drizzle/seeds';
import { JWT } from '$lib/server/jwt'; import { JWT } from '$lib/server/jwt';
import type { ThemeModeType } from '$lib/theme-mode';
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks'; import { sequence } from '@sveltejs/kit/hooks';
import { migrate } from 'drizzle-orm/mysql2/migrator'; import { migrate } from 'drizzle-orm/mysql2/migrator';
import { handleSession } from 'svelte-kit-cookie-session'; import { handleSession } from 'svelte-kit-cookie-session';
@ -18,6 +20,21 @@ if (AUTO_MIGRATE === 'true') {
await runSeeds(); await runSeeds();
const handleThemeHook = (async ({ resolve, event }) => {
// Take the theme from the query parameters or from the cookies
const newTheme = event.url.searchParams.get('themeMode') as ThemeModeType;
const cookieTheme = event.cookies.get('themeMode') as ThemeModeType;
const theme: ThemeModeType | null = newTheme || cookieTheme;
if (theme) {
return await resolve(event, {
transformPageChunk: ({ html }) => html.replace(/theme-base=""/g, `theme-base="${theme}"`)
});
}
return await resolve(event);
}) satisfies Handle;
export const handle = sequence( export const handle = sequence(
csrf(['/oauth2/token', '/oauth2/introspect', '/oauth2/device_authorization']), csrf(['/oauth2/token', '/oauth2/introspect', '/oauth2/device_authorization']),
handleSession({ handleSession({
@ -25,5 +42,6 @@ export const handle = sequence(
cookie: { cookie: {
secure: SESSION_SECURE === 'true' secure: SESSION_SECURE === 'true'
} }
}) }),
handleThemeHook
); );

View File

@ -1,16 +1,24 @@
<script lang="ts"> <script lang="ts">
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
import { setThemeMode, themeMode, type ThemeModeType } from '$lib/theme-mode'; import { themeMode, type ThemeModeType } from '$lib/theme-mode';
import Button from './Button.svelte'; import type { SubmitFunction } from '@sveltejs/kit';
import ActionButton from './ActionButton.svelte';
import Icon from './icons/Icon.svelte'; import Icon from './icons/Icon.svelte';
$: nextMode = ($themeMode === 'dark' ? 'light' : 'dark') as ThemeModeType; $: nextMode = ($themeMode === 'dark' ? 'light' : 'dark') as ThemeModeType;
$: iconName = $themeMode === 'light' ? 'DarkMode' : 'LightMode'; $: iconName = $themeMode === 'light' ? 'DarkMode' : 'LightMode';
const toggleMode = () => setThemeMode(nextMode); const enhanceFn: SubmitFunction = async ({ action, cancel }) => {
const theme = action.searchParams.get('themeMode') as ThemeModeType;
if (!theme) return;
themeMode.set(theme);
cancel();
};
</script> </script>
<Button variant="link" on:click={toggleMode}> <ActionButton action="/?/setTheme&themeMode={nextMode}" enhanced {enhanceFn}>
<Icon icon={iconName} /> <Icon icon={iconName} />
<span class="visually-hidden">{$t(`common.theme.${nextMode}`)}</span> <span class="visually-hidden">{$t(`common.theme.${nextMode}`)}</span></ActionButton
</Button> >

View File

@ -26,6 +26,7 @@
& .aside { & .aside {
display: flex; display: flex;
align-items: center;
gap: 1rem; gap: 1rem;
} }
} }

View File

@ -6,24 +6,46 @@ export type ThemeModeType = 'light' | 'dark';
export const themeMode = writable<ThemeModeType>('light', (set) => { export const themeMode = writable<ThemeModeType>('light', (set) => {
if (!browser) return; if (!browser) return;
const storageMode = window?.localStorage.getItem('inThemeMode') as ThemeModeType;
const uaTheme = window?.matchMedia?.('(prefers-color-scheme: dark)');
const uaMode: ThemeModeType = uaTheme?.matches ? 'dark' : 'light';
set(storageMode || uaMode || 'light'); set((document.documentElement.getAttribute('theme-base') as ThemeModeType) || 'light');
const uaThemeCallback = () => set(uaTheme?.matches ? 'dark' : 'light');
uaTheme?.addEventListener('change', uaThemeCallback);
return () => uaTheme?.removeEventListener('change', uaThemeCallback);
}); });
export const useThemeMode = () => /**
onMount(() => * Send a theme to the server so that it can be saved as a cookie.
themeMode.subscribe((value) => document.documentElement.setAttribute('theme-base', value)) * @param theme - theme to send to server
); */
export const forwardThemeToServer = async (theme: ThemeModeType) =>
fetch(`/?/setTheme&themeMode=${theme}`, {
method: 'POST',
body: new FormData(),
credentials: 'include',
headers: {
'x-sveltekit-action': 'true'
}
});
export const setThemeMode = (mode: ThemeModeType) => { export const forwardInitialTheme = () =>
if (!browser) return; onMount(() => {
themeMode.set(mode); // If a cookie-set theme is not present, set the theme to current user agent theme.
window.localStorage.setItem('inThemeMode', mode); const hasServerTheme = !!document.documentElement.getAttribute('theme-base');
const uaTheme = window?.matchMedia?.('(prefers-color-scheme: dark)');
const uaMode: ThemeModeType = uaTheme?.matches ? 'dark' : 'light';
const uaSetter = () => {
themeMode.set(uaTheme?.matches ? 'dark' : 'light');
}; };
if (!hasServerTheme) {
themeMode.set(uaMode);
}
const unsubscribeForward = themeMode.subscribe(async (theme) => {
document.documentElement.setAttribute('theme-base', theme);
await forwardThemeToServer(theme);
});
uaTheme?.addEventListener?.('change', uaSetter);
return () => {
unsubscribeForward();
uaTheme?.removeEventListener('change', uaSetter);
};
});

View File

@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import { forwardInitialTheme } from '$lib/theme-mode';
import '../app.css'; import '../app.css';
forwardInitialTheme();
</script> </script>
<slot></slot> <slot></slot>

View File

@ -1,16 +1,10 @@
import { browser } from '$app/environment';
import { loadTranslations } from '$lib/i18n'; import { loadTranslations } from '$lib/i18n';
import { themeMode } from '$lib/theme-mode';
export const load = async ({ url }) => { export const load = async ({ url }) => {
const { pathname } = url; const { pathname } = url;
const initLocale = 'en'; // get from cookie, user session, ... const initLocale = 'en'; // get from cookie, user session, ...
if (browser) {
themeMode.subscribe((value) => document.documentElement.setAttribute('theme-base', value));
}
await loadTranslations(initLocale, pathname); await loadTranslations(initLocale, pathname);
return {}; return {};

View File

@ -1,3 +1,20 @@
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export const actions = {
setTheme: async ({ url, cookies }) => {
// Set a new theme in the cookies according to themeMode query parameter
const themeMode = url.searchParams.get('themeMode');
if (themeMode) {
cookies.set('themeMode', themeMode, {
maxAge: 60 * 60 * 24 * 365,
httpOnly: true,
sameSite: 'lax',
path: '/'
});
}
return redirect(303, '/account');
}
};
export const load = () => redirect(302, '/account'); export const load = () => redirect(302, '/account');

View File

@ -109,9 +109,17 @@ export const actions = {
} as Actions; } as Actions;
export const load = async ({ locals, url }) => { export const load = async ({ locals, url }) => {
if (url.searchParams.has('redirectTo')) {
// Check that the redirect URL is a local path
if (!url.searchParams.get('redirectTo')?.startsWith('/')) {
return redirect(301, '/login');
}
// Redirect if already logged in
if (locals.session.data?.user) { if (locals.session.data?.user) {
return redirect(301, url.searchParams.get('redirectTo') || '/'); return redirect(301, url.searchParams.get('redirectTo') || '/');
} }
}
// Activation routine // Activation routine
if (url.searchParams.has('activate')) { if (url.searchParams.has('activate')) {