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 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}