The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
at main 106 lines 3.0 kB view raw
1'use client'; 2 3import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; 4 5export type Theme = 'light' | 'dark' | 'system'; 6 7export interface ThemeContextType { 8 theme: Theme; 9 setTheme: (theme: Theme) => void; 10} 11 12// Create default context values to avoid SSR errors 13const defaultContextValue: ThemeContextType = { 14 theme: 'system', 15 setTheme: () => {}, 16}; 17 18const ThemeContext = createContext<ThemeContextType>(defaultContextValue); 19 20export function ThemeProvider({ children }: { children: ReactNode }) { 21 const [theme, setTheme] = useState<Theme>('system'); 22 const [mounted, setMounted] = useState(false); 23 24 // Get stored theme preference or use system default 25 useEffect(() => { 26 // Only run in browser context 27 if (typeof window !== 'undefined') { 28 const storedTheme = localStorage.getItem('theme') as Theme | null; 29 if (storedTheme) { 30 setTheme(storedTheme); 31 } 32 setMounted(true); 33 } 34 }, []); 35 36 // Apply theme class to document 37 useEffect(() => { 38 if (!mounted || typeof window === 'undefined') return; 39 40 // Save to local storage 41 localStorage.setItem('theme', theme); 42 43 // Apply data-theme attribute 44 const root = window.document.documentElement; 45 46 if (theme === 'system') { 47 // Check system preference 48 const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 49 root.removeAttribute('data-theme'); 50 51 // Only add this class if needed to override system preference 52 if (systemPrefersDark) { 53 root.classList.add('dark'); 54 } else { 55 root.classList.remove('dark'); 56 } 57 } else { 58 // Apply explicit preference 59 root.setAttribute('data-theme', theme); 60 if (theme === 'dark') { 61 root.classList.add('dark'); 62 } else { 63 root.classList.remove('dark'); 64 } 65 } 66 }, [theme, mounted]); 67 68 // Handle system preference changes 69 useEffect(() => { 70 if (!mounted || typeof window === 'undefined') return; 71 72 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 73 74 const handleChange = () => { 75 if (theme === 'system') { 76 // Update the UI if we're in system mode 77 const root = window.document.documentElement; 78 if (mediaQuery.matches) { 79 root.classList.add('dark'); 80 } else { 81 root.classList.remove('dark'); 82 } 83 } 84 }; 85 86 mediaQuery.addEventListener('change', handleChange); 87 return () => mediaQuery.removeEventListener('change', handleChange); 88 }, [theme, mounted]); 89 90 const value = { 91 theme, 92 setTheme, 93 }; 94 95 // Always render the provider to avoid SSR issues, but use default values until mounted 96 return ( 97 <ThemeContext.Provider value={mounted ? value : defaultContextValue}> 98 {children} 99 </ThemeContext.Provider> 100 ); 101} 102 103// Safe version of useTheme that won't throw during SSR 104export function useTheme() { 105 return useContext(ThemeContext); 106}