Skip to content

Commit 1e4fd68

Browse files
committed
light/dark mode selector
1 parent 90444b4 commit 1e4fd68

File tree

5 files changed

+162
-16
lines changed

5 files changed

+162
-16
lines changed

frontend/src/App.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
import React from 'react';
22
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
33
import { AuthProvider } from './context/AuthContext';
4+
import { ThemeProvider } from './context/ThemeContext';
45
import { ProtectedRoute } from './components/auth/ProtectedRoute';
56
import { Dashboard } from './pages/Dashboard';
67
import { Login } from './pages/Login';
78

89
const App: React.FC = () => {
910
return (
1011
<BrowserRouter>
11-
<AuthProvider>
12-
<Routes>
13-
<Route path="/login" element={<Login />} />
14-
<Route
15-
path="/"
16-
element={
17-
<ProtectedRoute>
18-
<Dashboard />
19-
</ProtectedRoute>
20-
}
21-
/>
22-
<Route path="*" element={<Navigate to="/" replace />} />
23-
</Routes>
24-
</AuthProvider>
12+
<ThemeProvider>
13+
<AuthProvider>
14+
<Routes>
15+
<Route path="/login" element={<Login />} />
16+
<Route
17+
path="/"
18+
element={
19+
<ProtectedRoute>
20+
<Dashboard />
21+
</ProtectedRoute>
22+
}
23+
/>
24+
<Route path="*" element={<Navigate to="/" replace />} />
25+
</Routes>
26+
</AuthProvider>
27+
</ThemeProvider>
2528
</BrowserRouter>
2629
);
2730
};

frontend/src/components/layout/Header.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React, { useState } from 'react';
22
import { useAuth } from '../../hooks/useAuth';
3+
import { useTheme } from '../../hooks/useTheme';
34

45
export const Header: React.FC = () => {
56
const { user, logout } = useAuth();
7+
const { theme, setTheme } = useTheme();
68
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
79

810
const handleLogout = async () => {
@@ -68,7 +70,7 @@ export const Header: React.FC = () => {
6870
className="fixed inset-0 z-10"
6971
onClick={() => setIsDropdownOpen(false)}
7072
></div>
71-
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-20 border border-gray-200 dark:border-gray-700">
73+
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-20 border border-gray-200 dark:border-gray-700">
7274
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700">
7375
<p className="text-sm font-medium text-gray-900 dark:text-white">
7476
{getUserDisplayName()}
@@ -77,6 +79,63 @@ export const Header: React.FC = () => {
7779
<p className="text-xs text-gray-500 dark:text-gray-400">{user.email}</p>
7880
)}
7981
</div>
82+
83+
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
84+
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
85+
Theme
86+
</p>
87+
<div className="space-y-1">
88+
<button
89+
onClick={() => {
90+
setTheme('light');
91+
setIsDropdownOpen(false);
92+
}}
93+
className={`flex items-center w-full px-2 py-1.5 text-sm rounded transition-colors ${
94+
theme === 'light'
95+
? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300'
96+
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
97+
}`}
98+
>
99+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
100+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
101+
</svg>
102+
Light
103+
</button>
104+
<button
105+
onClick={() => {
106+
setTheme('dark');
107+
setIsDropdownOpen(false);
108+
}}
109+
className={`flex items-center w-full px-2 py-1.5 text-sm rounded transition-colors ${
110+
theme === 'dark'
111+
? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300'
112+
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
113+
}`}
114+
>
115+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
116+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
117+
</svg>
118+
Dark
119+
</button>
120+
<button
121+
onClick={() => {
122+
setTheme('system');
123+
setIsDropdownOpen(false);
124+
}}
125+
className={`flex items-center w-full px-2 py-1.5 text-sm rounded transition-colors ${
126+
theme === 'system'
127+
? 'bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300'
128+
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
129+
}`}
130+
>
131+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
132+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
133+
</svg>
134+
System
135+
</button>
136+
</div>
137+
</div>
138+
80139
<button
81140
onClick={handleLogout}
82141
className="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { createContext, useState, useEffect, ReactNode } from 'react';
2+
3+
type Theme = 'light' | 'dark' | 'system';
4+
5+
interface ThemeContextType {
6+
theme: Theme;
7+
setTheme: (theme: Theme) => void;
8+
effectiveTheme: 'light' | 'dark';
9+
}
10+
11+
export const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
12+
13+
interface ThemeProviderProps {
14+
children: ReactNode;
15+
}
16+
17+
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
18+
const [theme, setThemeState] = useState<Theme>('system');
19+
const [effectiveTheme, setEffectiveTheme] = useState<'light' | 'dark'>('light');
20+
21+
// Initialize theme from localStorage or default to system
22+
useEffect(() => {
23+
const savedTheme = localStorage.getItem('theme') as Theme | null;
24+
if (savedTheme && ['light', 'dark', 'system'].includes(savedTheme)) {
25+
setThemeState(savedTheme);
26+
}
27+
}, []);
28+
29+
// Update effective theme based on theme setting and system preference
30+
useEffect(() => {
31+
const updateEffectiveTheme = () => {
32+
if (theme === 'system') {
33+
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
34+
setEffectiveTheme(systemPrefersDark ? 'dark' : 'light');
35+
} else {
36+
setEffectiveTheme(theme);
37+
}
38+
};
39+
40+
updateEffectiveTheme();
41+
42+
// Listen for system preference changes when in system mode
43+
if (theme === 'system') {
44+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
45+
const handler = (e: MediaQueryListEvent) => {
46+
setEffectiveTheme(e.matches ? 'dark' : 'light');
47+
};
48+
49+
mediaQuery.addEventListener('change', handler);
50+
return () => mediaQuery.removeEventListener('change', handler);
51+
}
52+
}, [theme]);
53+
54+
// Apply theme to document
55+
useEffect(() => {
56+
const root = document.documentElement;
57+
if (effectiveTheme === 'dark') {
58+
root.classList.add('dark');
59+
} else {
60+
root.classList.remove('dark');
61+
}
62+
}, [effectiveTheme]);
63+
64+
const setTheme = (newTheme: Theme) => {
65+
setThemeState(newTheme);
66+
localStorage.setItem('theme', newTheme);
67+
};
68+
69+
return (
70+
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
71+
{children}
72+
</ThemeContext.Provider>
73+
);
74+
};

frontend/src/hooks/useTheme.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useContext } from 'react';
2+
import { ThemeContext } from '../context/ThemeContext';
3+
4+
export const useTheme = () => {
5+
const context = useContext(ThemeContext);
6+
if (context === undefined) {
7+
throw new Error('useTheme must be used within a ThemeProvider');
8+
}
9+
return context;
10+
};

frontend/tailwind.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default {
44
"./index.html",
55
"./src/**/*.{js,ts,jsx,tsx}",
66
],
7-
darkMode: 'media', // Use system preference
7+
darkMode: 'class', // Use class-based dark mode
88
theme: {
99
extend: {
1010
colors: {

0 commit comments

Comments
 (0)