diff --git a/src/app.html b/src/app.html
index 77a5ff5..8f524c6 100644
--- a/src/app.html
+++ b/src/app.html
@@ -1,5 +1,5 @@
-
+
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 92e3282..2b064d0 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -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
);
diff --git a/src/lib/components/ThemeButton.svelte b/src/lib/components/ThemeButton.svelte
index 6f9eb42..79d22f5 100644
--- a/src/lib/components/ThemeButton.svelte
+++ b/src/lib/components/ThemeButton.svelte
@@ -1,16 +1,24 @@
-
+ {$t(`common.theme.${nextMode}`)}
diff --git a/src/lib/components/admin/AdminHeader.svelte b/src/lib/components/admin/AdminHeader.svelte
index 38142cd..4d774b1 100644
--- a/src/lib/components/admin/AdminHeader.svelte
+++ b/src/lib/components/admin/AdminHeader.svelte
@@ -26,6 +26,7 @@
& .aside {
display: flex;
+ align-items: center;
gap: 1rem;
}
}
diff --git a/src/lib/theme-mode.ts b/src/lib/theme-mode.ts
index d631501..45e8cb3 100644
--- a/src/lib/theme-mode.ts
+++ b/src/lib/theme-mode.ts
@@ -6,24 +6,46 @@ export type ThemeModeType = 'light' | 'dark';
export const themeMode = writable('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);
+ };
+ });
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index c3bace0..6d1fdb8 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -1,5 +1,8 @@
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts
index 98d9ef4..82a3d48 100644
--- a/src/routes/+layout.ts
+++ b/src/routes/+layout.ts
@@ -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 {};
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
index 336c438..7490567 100644
--- a/src/routes/+page.server.ts
+++ b/src/routes/+page.server.ts
@@ -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');
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts
index d453d3c..c6f05a9 100644
--- a/src/routes/login/+page.server.ts
+++ b/src/routes/login/+page.server.ts
@@ -109,8 +109,16 @@ export const actions = {
} as Actions;
export const load = async ({ locals, url }) => {
- if (locals.session.data?.user) {
- return redirect(301, url.searchParams.get('redirectTo') || '/');
+ 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