Theme persistance in cookie
This commit is contained in:
parent
3b16762f0e
commit
8dd6ccc58c
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" theme-base="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
|
@ -3,6 +3,8 @@ import { csrf } from '$lib/server/csrf';
|
||||
import { DB } from '$lib/server/drizzle';
|
||||
import { runSeeds } from '$lib/server/drizzle/seeds';
|
||||
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 { migrate } from 'drizzle-orm/mysql2/migrator';
|
||||
import { handleSession } from 'svelte-kit-cookie-session';
|
||||
@ -18,6 +20,21 @@ if (AUTO_MIGRATE === 'true') {
|
||||
|
||||
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(
|
||||
csrf(['/oauth2/token', '/oauth2/introspect', '/oauth2/device_authorization']),
|
||||
handleSession({
|
||||
@ -25,5 +42,6 @@ export const handle = sequence(
|
||||
cookie: {
|
||||
secure: SESSION_SECURE === 'true'
|
||||
}
|
||||
})
|
||||
}),
|
||||
handleThemeHook
|
||||
);
|
||||
|
@ -1,16 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n';
|
||||
import { setThemeMode, themeMode, type ThemeModeType } from '$lib/theme-mode';
|
||||
import Button from './Button.svelte';
|
||||
import { themeMode, type ThemeModeType } from '$lib/theme-mode';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
import ActionButton from './ActionButton.svelte';
|
||||
import Icon from './icons/Icon.svelte';
|
||||
|
||||
$: nextMode = ($themeMode === 'dark' ? 'light' : 'dark') as ThemeModeType;
|
||||
$: 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>
|
||||
|
||||
<Button variant="link" on:click={toggleMode}>
|
||||
<ActionButton action="/?/setTheme&themeMode={nextMode}" enhanced {enhanceFn}>
|
||||
<Icon icon={iconName} />
|
||||
<span class="visually-hidden">{$t(`common.theme.${nextMode}`)}</span>
|
||||
</Button>
|
||||
<span class="visually-hidden">{$t(`common.theme.${nextMode}`)}</span></ActionButton
|
||||
>
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
& .aside {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
@ -6,24 +6,46 @@ export type ThemeModeType = 'light' | 'dark';
|
||||
|
||||
export const themeMode = writable<ThemeModeType>('light', (set) => {
|
||||
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');
|
||||
|
||||
const uaThemeCallback = () => set(uaTheme?.matches ? 'dark' : 'light');
|
||||
uaTheme?.addEventListener('change', uaThemeCallback);
|
||||
return () => uaTheme?.removeEventListener('change', uaThemeCallback);
|
||||
set((document.documentElement.getAttribute('theme-base') as ThemeModeType) || 'light');
|
||||
});
|
||||
|
||||
export const useThemeMode = () =>
|
||||
onMount(() =>
|
||||
themeMode.subscribe((value) => document.documentElement.setAttribute('theme-base', value))
|
||||
);
|
||||
/**
|
||||
* Send a theme to the server so that it can be saved as a cookie.
|
||||
* @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) => {
|
||||
if (!browser) return;
|
||||
themeMode.set(mode);
|
||||
window.localStorage.setItem('inThemeMode', mode);
|
||||
export const forwardInitialTheme = () =>
|
||||
onMount(() => {
|
||||
// If a cookie-set theme is not present, set the theme to current user agent theme.
|
||||
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);
|
||||
};
|
||||
});
|
||||
|
@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { forwardInitialTheme } from '$lib/theme-mode';
|
||||
import '../app.css';
|
||||
|
||||
forwardInitialTheme();
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
||||
|
@ -1,16 +1,10 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { loadTranslations } from '$lib/i18n';
|
||||
import { themeMode } from '$lib/theme-mode';
|
||||
|
||||
export const load = async ({ url }) => {
|
||||
const { pathname } = url;
|
||||
|
||||
const initLocale = 'en'; // get from cookie, user session, ...
|
||||
|
||||
if (browser) {
|
||||
themeMode.subscribe((value) => document.documentElement.setAttribute('theme-base', value));
|
||||
}
|
||||
|
||||
await loadTranslations(initLocale, pathname);
|
||||
|
||||
return {};
|
||||
|
@ -1,3 +1,20 @@
|
||||
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');
|
||||
|
@ -109,9 +109,17 @@ export const actions = {
|
||||
} as Actions;
|
||||
|
||||
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) {
|
||||
return redirect(301, url.searchParams.get('redirectTo') || '/');
|
||||
}
|
||||
}
|
||||
|
||||
// Activation routine
|
||||
if (url.searchParams.has('activate')) {
|
||||
|
Loading…
Reference in New Issue
Block a user