feat(09-05): ThemeSessionBridge + ThemeToggle write-through to /api/me/theme
- Create ThemeSessionBridge.tsx: useEffect compares session.user.theme to
next-themes value; calls setTheme(serverTheme) on mismatch; validates
against 3-string allowlist ('light'|'dark'|'system'); renders null
- Mount <ThemeSessionBridge /> as first child of <AuthProvider> in app/layout.tsx
- Modify ThemeToggle: writeTheme() calls setTheme() then fire-and-forget
PUT /api/me/theme; silent catch for network errors (best-effort desktop UX)
This commit is contained in:
parent
1b7c453c6d
commit
586c04ad2a
3 changed files with 58 additions and 3 deletions
|
|
@ -7,6 +7,7 @@ import { CommandPalette } from "@/components/navigation/command-palette";
|
|||
import { TaglineFooter } from "@/components/branding/tagline-footer";
|
||||
import { Toaster } from "sonner";
|
||||
import { AuthProvider } from "@/components/auth/auth-provider";
|
||||
import { ThemeSessionBridge } from "@/components/mobile/profile/ThemeSessionBridge";
|
||||
|
||||
// IBM Plex Sans replaces the brand-mandated Helvetica/Arial. The 2013
|
||||
// standards guide called for Helvetica Bold for headers and Helvetica
|
||||
|
|
@ -69,6 +70,7 @@ export default function RootLayout({
|
|||
disableTransitionOnChange
|
||||
>
|
||||
<AuthProvider>
|
||||
<ThemeSessionBridge />
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<AppNavigation />
|
||||
<main className="flex-1">{children}</main>
|
||||
|
|
|
|||
38
components/mobile/profile/ThemeSessionBridge.tsx
Normal file
38
components/mobile/profile/ThemeSessionBridge.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
'use client';
|
||||
|
||||
/**
|
||||
* ThemeSessionBridge (THEME-03 / D-16, D-17).
|
||||
*
|
||||
* On session load and after sign-in, compares session.user.theme to the
|
||||
* next-themes useTheme() value and calls setTheme(session.user.theme) if
|
||||
* different. Server is canonical; this is the bridge that enforces it.
|
||||
*
|
||||
* Renders nothing.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
|
||||
type SessionUserWithTheme = { theme?: 'light' | 'dark' | 'system' | string };
|
||||
|
||||
export function ThemeSessionBridge() {
|
||||
const { data: session } = useSession();
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.user) return;
|
||||
const serverTheme = (session.user as SessionUserWithTheme).theme;
|
||||
if (
|
||||
serverTheme === 'light' ||
|
||||
serverTheme === 'dark' ||
|
||||
serverTheme === 'system'
|
||||
) {
|
||||
if (serverTheme !== theme) {
|
||||
setTheme(serverTheme);
|
||||
}
|
||||
}
|
||||
}, [session?.user, theme, setTheme]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -15,6 +15,21 @@ import {
|
|||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme()
|
||||
|
||||
// Write the new theme choice through to the server so session.user.theme
|
||||
// stays canonical. Fire-and-forget — ThemeSessionBridge re-syncs from
|
||||
// session on next session refresh if the request fails.
|
||||
const writeTheme = (next: 'light' | 'dark' | 'system') => {
|
||||
setTheme(next)
|
||||
fetch('/api/me/theme', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ theme: next }),
|
||||
}).catch(() => {
|
||||
// Silent fail on network error — the desktop affordance is best-effort.
|
||||
// The mobile profile Theme section is the explicit-error UX.
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
@ -25,15 +40,15 @@ export function ThemeToggle() {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
<DropdownMenuItem onClick={() => writeTheme("light")}>
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
<span>Light</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
<DropdownMenuItem onClick={() => writeTheme("dark")}>
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
<span>Dark</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<DropdownMenuItem onClick={() => writeTheme("system")}>
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
<span>System</span>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue