1import { useEffect, useState } from 'react';
2import './ThemeSelector.css';
3import { apiFetch } from '../lib/api';
5 { id: 'system', name: 'System', icon: 'desktop_windows' },
6 { id: 'light', name: 'Light', icon: 'light_mode' },
7 { id: 'dark', name: 'Dark', icon: 'dark_mode' },
8 { id: 'blue', name: 'Blue', icon: 'palette' },
9 { id: 'green', name: 'Green', icon: 'forest' },
10 { id: 'contrast', name: 'High Contrast', icon: 'contrast' },
11 { id: 'colorblind', name: 'Colorblind', icon: 'accessibility' }
14export default function ThemeSelector() {
15 const [currentTheme, setCurrentTheme] = useState('system');
16 const [isOpen, setIsOpen] = useState(false);
19 // Always fetch theme from server if authenticated
20 const token = localStorage.getItem('token');
22 apiFetch('/users/me/theme', {
23 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
26 if (!r.ok) throw new Error('no theme');
30 const serverTheme = json.theme || 'system';
31 setCurrentTheme(serverTheme);
32 applyTheme(serverTheme);
35 setCurrentTheme('system');
39 setCurrentTheme('system');
42 // Listen for JWT changes (e.g., after impersonation)
43 const handleJwtChange = () => {
44 const newToken = localStorage.getItem('token');
46 apiFetch('/users/me/theme', {
47 headers: { Authorization: `Bearer ${newToken}`, 'Content-Type': 'application/json' }
49 .then(r => r.ok ? r.json() : { theme: 'system' })
51 const serverTheme = json.theme || 'system';
52 setCurrentTheme(serverTheme);
53 applyTheme(serverTheme);
56 setCurrentTheme('system');
60 window.addEventListener('storage', handleJwtChange);
61 return () => window.removeEventListener('storage', handleJwtChange);
64 const applyTheme = (themeId) => {
65 if (themeId === 'system') {
66 // Apply theme based on system preference
67 const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
68 document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
70 document.documentElement.setAttribute('data-theme', themeId);
74 const handleThemeChange = (themeId) => {
75 setCurrentTheme(themeId);
79 // Persist to server if logged in
80 const token = localStorage.getItem('token');
82 apiFetch('/users/me/theme', {
84 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
85 body: JSON.stringify({ theme: themeId })
86 }).catch(err => console.warn('Failed to persist theme:', err));
90 const currentThemeData = themes.find(t => t.id === currentTheme);
93 <div className="theme-selector">
95 className="theme-button"
96 onClick={() => setIsOpen(!isOpen)}
99 <span className="material-symbols-outlined">{currentThemeData.icon}</span>
103 <div className="theme-dropdown">
104 {themes.map(theme => (
107 className={`theme-option ${theme.id === currentTheme ? 'active' : ''}`}
108 onClick={() => handleThemeChange(theme.id)}
110 <span className="material-symbols-outlined">{theme.icon}</span>