forked from
atpota.to/flushes.app
The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
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}