Skip to content

Commit 715ac4a

Browse files
committed
refactor: enhance theme context to support theme mode management
1 parent 00d148e commit 715ac4a

File tree

1 file changed

+67
-6
lines changed

1 file changed

+67
-6
lines changed

contexts/theme-context.tsx

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
import React, { createContext, useContext, useEffect, useReducer, useCallback } from 'react'
44

55
export type ThemeColor = 'red' | 'amber' | 'yellow' | 'cyan' | 'green' | 'indigo' | 'purple' | 'pink' | 'blue' | 'slate'
6+
export type ThemeMode = 'light' | 'dark' | 'system'
67

78
interface ThemeState {
89
themeColor: ThemeColor
10+
themeMode: ThemeMode
911
isDark: boolean
1012
isInitialized: boolean
1113
}
1214

1315
type ThemeAction =
1416
| { type: 'SET_THEME_COLOR'; color: ThemeColor }
17+
| { type: 'SET_THEME_MODE'; mode: ThemeMode }
1518
| { type: 'SET_DARK_MODE'; isDark: boolean }
16-
| { type: 'INITIALIZE'; themeColor: ThemeColor; isDark: boolean }
19+
| { type: 'INITIALIZE'; themeColor: ThemeColor; themeMode: ThemeMode; isDark: boolean }
1720

1821
interface ThemeContextType {
1922
themeColor: ThemeColor
23+
themeMode: ThemeMode
2024
setThemeColor: (color: ThemeColor) => void
25+
setThemeMode: (mode: ThemeMode) => void
2126
}
2227

2328
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
@@ -38,11 +43,14 @@ function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
3843
switch (action.type) {
3944
case 'SET_THEME_COLOR':
4045
return { ...state, themeColor: action.color }
46+
case 'SET_THEME_MODE':
47+
return { ...state, themeMode: action.mode }
4148
case 'SET_DARK_MODE':
4249
return { ...state, isDark: action.isDark }
4350
case 'INITIALIZE':
4451
return {
4552
themeColor: action.themeColor,
53+
themeMode: action.themeMode,
4654
isDark: action.isDark,
4755
isInitialized: true
4856
}
@@ -54,6 +62,7 @@ function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
5462
export function ThemeProvider({ children }: ThemeProviderProps) {
5563
const [state, dispatch] = useReducer(themeReducer, {
5664
themeColor: 'blue',
65+
themeMode: 'system',
5766
isDark: false,
5867
isInitialized: false
5968
})
@@ -62,6 +71,25 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
6271
dispatch({ type: 'SET_THEME_COLOR', color })
6372
}, [])
6473

74+
const setThemeMode = useCallback((mode: ThemeMode) => {
75+
dispatch({ type: 'SET_THEME_MODE', mode })
76+
77+
// Apply the theme mode immediately
78+
if (mode === 'light') {
79+
document.documentElement.classList.remove('dark')
80+
} else if (mode === 'dark') {
81+
document.documentElement.classList.add('dark')
82+
} else {
83+
// System mode - check system preference
84+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches
85+
if (systemDark) {
86+
document.documentElement.classList.add('dark')
87+
} else {
88+
document.documentElement.classList.remove('dark')
89+
}
90+
}
91+
}, [])
92+
6593
// Initialize theme from localStorage
6694
useEffect(() => {
6795
try {
@@ -70,18 +98,25 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
7098
? savedTheme
7199
: 'blue'
72100

101+
const savedMode = localStorage.getItem('theme-mode') as ThemeMode
102+
const validMode = savedMode && ['light', 'dark', 'system'].includes(savedMode)
103+
? savedMode
104+
: 'system'
105+
73106
const isDarkMode = document.documentElement.classList.contains('dark')
74107

75108
dispatch({
76109
type: 'INITIALIZE',
77-
themeColor: validTheme,
110+
themeColor: validTheme,
111+
themeMode: validMode,
78112
isDark: isDarkMode
79113
})
80114
} catch (error) {
81115
console.warn('Failed to load theme from localStorage:', error)
82116
dispatch({
83117
type: 'INITIALIZE',
84-
themeColor: 'blue',
118+
themeColor: 'blue',
119+
themeMode: 'system',
85120
isDark: false
86121
})
87122
}
@@ -93,12 +128,13 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
93128

94129
try {
95130
localStorage.setItem('theme-color', state.themeColor)
131+
localStorage.setItem('theme-mode', state.themeMode)
96132
} catch (error) {
97133
console.warn('Failed to save theme to localStorage:', error)
98134
}
99135

100136
applyThemeVariables(state.themeColor, state.isDark)
101-
}, [state.themeColor, state.isDark, state.isInitialized])
137+
}, [state.themeColor, state.themeMode, state.isDark, state.isInitialized])
102138

103139
// Watch for dark mode changes with debouncing
104140
useEffect(() => {
@@ -131,10 +167,35 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
131167
}
132168
}, [state.isDark, state.isInitialized])
133169

170+
// Watch for system theme preference changes
171+
useEffect(() => {
172+
if (!state.isInitialized || state.themeMode !== 'system') return
173+
174+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
175+
176+
const handleChange = (e: MediaQueryListEvent) => {
177+
if (state.themeMode === 'system') {
178+
if (e.matches) {
179+
document.documentElement.classList.add('dark')
180+
} else {
181+
document.documentElement.classList.remove('dark')
182+
}
183+
}
184+
}
185+
186+
mediaQuery.addEventListener('change', handleChange)
187+
188+
return () => {
189+
mediaQuery.removeEventListener('change', handleChange)
190+
}
191+
}, [state.themeMode, state.isInitialized])
192+
134193
return (
135194
<ThemeContext.Provider value={{
136-
themeColor: state.themeColor,
137-
setThemeColor
195+
themeColor: state.themeColor,
196+
themeMode: state.themeMode,
197+
setThemeColor,
198+
setThemeMode
138199
}}>
139200
{children}
140201
</ThemeContext.Provider>

0 commit comments

Comments
 (0)