← back to Angels Flowers
components/ThemeProvider.tsx
123 lines
'use client'
import { createContext, useContext, useEffect, useState, ReactNode, useCallback } from 'react'
type Theme = 'light' | 'dark'
interface ThemeContextType {
theme: Theme
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
interface ThemeProviderProps {
children: ReactNode
}
function getInitialTheme(): Theme {
if (typeof window === 'undefined') {
return 'light'
}
try {
const savedTheme = localStorage.getItem('theme') as Theme | null
if (savedTheme === 'light' || savedTheme === 'dark') {
return savedTheme
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark'
}
} catch {
// Handle cases where localStorage is not available (e.g., private browsing)
}
return 'light'
}
export default function ThemeProvider({ children }: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(getInitialTheme)
const [mounted, setMounted] = useState(false)
// Apply theme to document
const applyTheme = useCallback((newTheme: Theme) => {
const root = document.documentElement
root.setAttribute('data-theme', newTheme)
if (newTheme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}, [])
// Initialize on mount
useEffect(() => {
setMounted(true)
// Sync with what the inline script may have already set
const currentTheme = getInitialTheme()
setTheme(currentTheme)
applyTheme(currentTheme)
}, [applyTheme])
// Listen for system theme changes
useEffect(() => {
if (!mounted) return
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => {
// Only update if user hasn't set a preference
const savedTheme = localStorage.getItem('theme')
if (!savedTheme) {
const newTheme = e.matches ? 'dark' : 'light'
setTheme(newTheme)
applyTheme(newTheme)
}
}
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}
// Fallback for older browsers
mediaQuery.addListener(handleChange)
return () => mediaQuery.removeListener(handleChange)
}, [mounted, applyTheme])
// Persist theme changes
useEffect(() => {
if (!mounted) return
applyTheme(theme)
try {
localStorage.setItem('theme', theme)
} catch {
// Handle localStorage unavailable
}
}, [theme, mounted, applyTheme])
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}, [])
// Render children even before mount to avoid layout shift
// The inline script in layout.tsx handles the initial theme
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}