EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
ThemeSelector.jsx
Go to the documentation of this file.
1import { useEffect, useState } from 'react';
2import './ThemeSelector.css';
3import { apiFetch } from '../lib/api';
4const themes = [
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' }
12];
13
14export default function ThemeSelector() {
15 const [currentTheme, setCurrentTheme] = useState('system');
16 const [isOpen, setIsOpen] = useState(false);
17
18 useEffect(() => {
19 // Always fetch theme from server if authenticated
20 const token = localStorage.getItem('token');
21 if (token) {
22 apiFetch('/users/me/theme', {
23 headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }
24 })
25 .then(r => {
26 if (!r.ok) throw new Error('no theme');
27 return r.json();
28 })
29 .then(json => {
30 const serverTheme = json.theme || 'system';
31 setCurrentTheme(serverTheme);
32 applyTheme(serverTheme);
33 })
34 .catch(() => {
35 setCurrentTheme('system');
36 applyTheme('system');
37 });
38 } else {
39 setCurrentTheme('system');
40 applyTheme('system');
41 }
42 // Listen for JWT changes (e.g., after impersonation)
43 const handleJwtChange = () => {
44 const newToken = localStorage.getItem('token');
45 if (newToken) {
46 apiFetch('/users/me/theme', {
47 headers: { Authorization: `Bearer ${newToken}`, 'Content-Type': 'application/json' }
48 })
49 .then(r => r.ok ? r.json() : { theme: 'system' })
50 .then(json => {
51 const serverTheme = json.theme || 'system';
52 setCurrentTheme(serverTheme);
53 applyTheme(serverTheme);
54 });
55 } else {
56 setCurrentTheme('system');
57 applyTheme('system');
58 }
59 };
60 window.addEventListener('storage', handleJwtChange);
61 return () => window.removeEventListener('storage', handleJwtChange);
62 }, []);
63
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');
69 } else {
70 document.documentElement.setAttribute('data-theme', themeId);
71 }
72 };
73
74 const handleThemeChange = (themeId) => {
75 setCurrentTheme(themeId);
76 applyTheme(themeId);
77 setIsOpen(false);
78
79 // Persist to server if logged in
80 const token = localStorage.getItem('token');
81 if (token) {
82 apiFetch('/users/me/theme', {
83 method: 'PUT',
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));
87 }
88 };
89
90 const currentThemeData = themes.find(t => t.id === currentTheme);
91
92 return (
93 <div className="theme-selector">
94 <button
95 className="theme-button"
96 onClick={() => setIsOpen(!isOpen)}
97 title="Change theme"
98 >
99 <span className="material-symbols-outlined">{currentThemeData.icon}</span>
100 </button>
101
102 {isOpen && (
103 <div className="theme-dropdown">
104 {themes.map(theme => (
105 <button
106 key={theme.id}
107 className={`theme-option ${theme.id === currentTheme ? 'active' : ''}`}
108 onClick={() => handleThemeChange(theme.id)}
109 >
110 <span className="material-symbols-outlined">{theme.icon}</span>
111 {theme.name}
112 </button>
113 ))}
114 </div>
115 )}
116 </div>
117 );
118}