All files / src/contexts ThemeContext.tsx

99.05% Statements 209/211
86.84% Branches 33/38
100% Functions 3/3
99.05% Lines 209/211

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 2121x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 136x 136x 136x 136x 136x 136x 136x 33x 33x 136x 136x 136x 136x 89x 33x 33x 56x 136x 136x 136x 136x 80x 80x 80x 80x 32x 80x 48x 48x 80x 80x 80x 80x 80x 80x 80x     136x 136x 136x 136x 23x 23x 23x 23x 23x 23x 2x 2x 2x 23x 23x 23x 23x 23x 136x 136x 136x 136x 10x 10x 10x 136x 136x 136x 136x 56x 56x 56x 56x 56x 2x 2x 56x 3x 3x 56x 56x 56x 56x 56x 56x 56x 56x 136x 136x 136x 136x 81x 32x 32x 32x 32x 1x 1x 1x 32x 32x 32x 58x 31x 31x 31x 1x 1x 1x 1x 1x 136x 136x 136x 136x 112x 56x 56x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 136x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 140x 140x 140x 140x 4x 4x 135x 135x 135x  
/**
 * Theme Context
 *
 * Provides theme management (light/dark mode) throughout the application.
 *
 * Features:
 * - System preference detection
 * - Manual theme toggle
 * - localStorage persistence
 * - SSR-safe implementation
 */
 
'use client';
 
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { clientLogger } from '@/lib/logging/clientLogger';
 
export type Theme = 'light' | 'dark' | 'system';
export type ResolvedTheme = 'light' | 'dark';
 
interface ThemeContextValue {
  /** Current theme setting (light, dark, or system) */
  theme: Theme;
  /** Resolved theme after considering system preference */
  resolvedTheme: ResolvedTheme;
  /** Function to set the theme */
  setTheme: (theme: Theme) => void;
  /** Function to toggle between light and dark */
  toggleTheme: () => void;
}
 
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
 
const STORAGE_KEY = 'elite-events-theme';
 
interface ThemeProviderProps {
  children: React.ReactNode;
  /** Default theme to use */
  defaultTheme?: Theme;
}
 
/**
 * Theme Provider Component
 *
 * Wrap your app with this provider to enable theme management
 *
 * @example
 * ```tsx
 * <ThemeProvider defaultTheme="system">
 *   <App />
 * </ThemeProvider>
 * ```
 */
export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProviderProps) {
  const [theme, setThemeState] = useState<Theme>(defaultTheme);
  const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');
  const [mounted, setMounted] = useState(false);
 
  // Get system theme preference
  const getSystemTheme = useCallback((): ResolvedTheme => {
    if (typeof window === 'undefined') return 'light';
    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
  }, []);
 
  // Resolve theme based on setting and system preference
  const resolveTheme = useCallback((themeValue: Theme): ResolvedTheme => {
    if (themeValue === 'system') {
      return getSystemTheme();
    }
    return themeValue;
  }, [getSystemTheme]);
 
  // Apply theme to document
  const applyTheme = useCallback((resolved: ResolvedTheme) => {
    const root = document.documentElement;
 
    // Add/remove 'dark' class for Tailwind CSS dark mode
    if (resolved === 'dark') {
      root.classList.add('dark');
    } else {
      root.classList.remove('dark');
    }
 
    // Also set data-theme attribute for any custom CSS selectors
    root.setAttribute('data-theme', resolved);
 
    // Also update meta theme-color for mobile browsers
    const metaTheme = document.querySelector('meta[name="theme-color"]');
    if (metaTheme) {
      metaTheme.setAttribute('content', resolved === 'dark' ? '#0f172a' : '#ffffff');
    }
  }, []);
 
  // Set theme function
  const setTheme = useCallback((newTheme: Theme) => {
    setThemeState(newTheme);
 
    // Save to localStorage
    try {
      localStorage.setItem(STORAGE_KEY, newTheme);
    } catch (error) {
      // localStorage might not be available
      clientLogger.error('Failed to save theme preference', error instanceof Error ? error : new Error(String(error)));
    }
 
    // Apply the resolved theme
    const resolved = resolveTheme(newTheme);
    setResolvedTheme(resolved);
    applyTheme(resolved);
  }, [resolveTheme, applyTheme]);
 
  // Toggle theme function
  const toggleTheme = useCallback(() => {
    const currentResolved = resolveTheme(theme);
    const newTheme: Theme = currentResolved === 'dark' ? 'light' : 'dark';
    setTheme(newTheme);
  }, [theme, resolveTheme, setTheme]);
 
  // Initialize theme on mount
  useEffect(() => {
    // Load saved theme from localStorage
    let initialTheme = defaultTheme;
    try {
      const saved = localStorage.getItem(STORAGE_KEY) as Theme | null;
      if (saved && ['light', 'dark', 'system'].includes(saved)) {
        initialTheme = saved;
      }
    } catch (error) {
      clientLogger.error('Failed to load theme preference', error instanceof Error ? error : new Error(String(error)));
    }
 
    // Apply initial theme
    setThemeState(initialTheme);
    const resolved = resolveTheme(initialTheme);
    setResolvedTheme(resolved);
    applyTheme(resolved);
    setMounted(true);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // Run only once on mount
 
  // Listen for system theme changes when theme is set to 'system'
  useEffect(() => {
    if (theme !== 'system') return;
 
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
 
    const handleChange = (e: MediaQueryListEvent) => {
      const newResolved = e.matches ? 'dark' : 'light';
      setResolvedTheme(newResolved);
      applyTheme(newResolved);
    };
 
    // Modern browsers
    if (mediaQuery.addEventListener) {
      mediaQuery.addEventListener('change', handleChange);
      return () => mediaQuery.removeEventListener('change', handleChange);
    }
    // Legacy browsers
    else if (mediaQuery.addListener) {
      mediaQuery.addListener(handleChange);
      return () => mediaQuery.removeListener(handleChange);
    }
  }, [theme, applyTheme]);
 
  // Prevent flash of unstyled content
  useEffect(() => {
    if (mounted) {
      document.documentElement.classList.add('theme-loaded');
    }
  }, [mounted]);
 
  const value: ThemeContextValue = {
    theme,
    resolvedTheme,
    setTheme,
    toggleTheme};
 
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
 
/**
 * useTheme Hook
 *
 * Access theme context in any component
 *
 * @example
 * ```tsx
 * function MyComponent() {
 *   const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();
 *
 *   return (
 *     <button onClick={toggleTheme}>
 *       Current theme: {resolvedTheme}
 *     </button>
 *   );
 * }
 * ```
 */
export function useTheme() {
  const context = useContext(ThemeContext);
 
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
 
  return context;
}