Theme persistance in cookie
This commit is contained in:
parent
3b16762f0e
commit
8dd6ccc58c
@ -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" />
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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>
|
>
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
& .aside {
|
& .aside {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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 {};
|
||||||
|
@ -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');
|
||||||
|
@ -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')) {
|
||||||
|
Loading…
Reference in New Issue
Block a user