The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
at main 109 lines 3.7 kB view raw
1'use client'; 2 3import { useTheme, ThemeContextType, Theme } from '@/lib/theme-context'; 4import React, { useState, useEffect } from 'react'; 5import styles from './ThemeToggle.module.css'; 6 7// Default light theme icon as fallback for static rendering 8const LightIcon = () => ( 9 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 10 <circle cx="12" cy="12" r="5"></circle> 11 <line x1="12" y1="1" x2="12" y2="3"></line> 12 <line x1="12" y1="21" x2="12" y2="23"></line> 13 <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> 14 <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> 15 <line x1="1" y1="12" x2="3" y2="12"></line> 16 <line x1="21" y1="12" x2="23" y2="12"></line> 17 <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> 18 <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> 19 </svg> 20); 21 22// Dark theme icon 23const DarkIcon = () => ( 24 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 25 <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> 26 </svg> 27); 28 29// System theme icon 30const SystemIcon = () => ( 31 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 32 <circle cx="12" cy="12" r="10"></circle> 33 <line x1="2" y1="12" x2="22" y2="12"></line> 34 <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> 35 </svg> 36); 37 38export default function ThemeToggle() { 39 // Prevent hydration errors by using conditional hooks 40 const [mounted, setMounted] = useState(false); 41 const [themeState, setThemeState] = useState<Theme>('system'); 42 43 // Safe way to access theme context that won't break SSR 44 let themeContext: ThemeContextType | undefined = undefined; 45 try { 46 themeContext = useTheme(); 47 } catch (e) { 48 // During SSR, the context won't be available, and that's ok 49 } 50 51 useEffect(() => { 52 setMounted(true); 53 if (themeContext) { 54 setThemeState(themeContext.theme); 55 } 56 }, [themeContext]); 57 58 const toggleTheme = () => { 59 if (!themeContext) return; 60 61 if (themeState === 'light') { 62 themeContext.setTheme('dark'); 63 setThemeState('dark'); 64 } else if (themeState === 'dark') { 65 themeContext.setTheme('system'); 66 setThemeState('system'); 67 } else { 68 themeContext.setTheme('light'); 69 setThemeState('light'); 70 } 71 }; 72 73 const getIcon = () => { 74 if (themeState === 'light') { 75 return <LightIcon />; 76 } else if (themeState === 'dark') { 77 return <DarkIcon />; 78 } else { 79 return <SystemIcon />; 80 } 81 }; 82 83 const getLabel = () => { 84 if (themeState === 'light') return 'Lights On'; 85 if (themeState === 'dark') return 'Lights Off'; 86 return 'System Lights'; 87 }; 88 89 // During SSR or before mounting, render a placeholder that won't try to use the context 90 if (!mounted) { 91 return ( 92 <button className={`${styles.themeToggle} font-medium`} aria-label="Theme toggle"> 93 <LightIcon /> 94 <span className={`${styles.themeLabel} font-medium`}>Lights On</span> 95 </button> 96 ); 97 } 98 99 return ( 100 <button 101 className={`${styles.themeToggle} font-medium`} 102 onClick={toggleTheme} 103 aria-label={`Switch to ${themeState === 'light' ? 'dark' : themeState === 'dark' ? 'system' : 'light'} theme`} 104 > 105 {getIcon()} 106 <span className={`${styles.themeLabel} font-medium`}>{getLabel()}</span> 107 </button> 108 ); 109}