diff --git a/frontend/src/App.css b/frontend/src/App.css index 3ab011c..38c69fc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,6 +1,43 @@ +:root { + /* Light theme variables */ + --bg-primary: #f9fafb; + --bg-secondary: #ffffff; + --text-primary: #111827; + --text-secondary: #4b5563; + --accent-primary: #3b82f6; + --accent-secondary: #60a5fa; + --border-color: #e5e7eb; + --shadow-color: rgba(0, 0, 0, 0.1); + + /* Animation durations */ + --transition-duration: 300ms; + --transition-timing: cubic-bezier(0.4, 0, 0.2, 1); +} + +:root[class~="dark"] { + /* Dark theme variables */ + --bg-primary: #111827; + --bg-secondary: #1f2937; + --text-primary: #f9fafb; + --text-secondary: #d1d5db; + --accent-primary: #60a5fa; + --accent-secondary: #93c5fd; + --border-color: #374151; + --shadow-color: rgba(0, 0, 0, 0.25); +} + +/* Apply transitions to theme changes */ +*, *::before, *::after { + transition: background-color var(--transition-duration) var(--transition-timing), + border-color var(--transition-duration) var(--transition-timing), + color var(--transition-duration) var(--transition-timing), + box-shadow var(--transition-duration) var(--transition-timing); +} + .App { min-height: 100vh; - background-color: #f9fafb; + background-color: var(--bg-primary); + color: var(--text-primary); } /* Loading spinner */ diff --git a/frontend/src/App.js b/frontend/src/App.js index bd0f0cc..349dbaa 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { Toaster } from 'react-hot-toast'; import Layout from './components/Layout/Layout'; +import { getUserPreferences, setUserPreferences } from './services/userPreferences'; import SortingVisualizer from './pages/SortingVisualizer'; import GraphVisualizer from './pages/GraphVisualizer'; import StringVisualizer from './pages/StringVisualizer'; @@ -13,71 +14,88 @@ import ContributorsPage from './pages/ContributorsPage'; import './App.css'; function App() { - // Dark mode state with persistence - const [darkMode, setDarkMode] = useState(() => { + // Theme state with persistence + const [theme, setTheme] = useState(() => { + // Check system preference first + const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; try { - const saved = localStorage.getItem('darkMode'); - return saved ? JSON.parse(saved) : false; + const { theme: savedTheme } = getUserPreferences(); + return savedTheme || systemPreference; } catch (error) { - console.error('Error loading dark mode preference:', error); - return false; + console.error('Error loading theme preference:', error); + return systemPreference; } }); - // Save dark mode preference - useEffect(() => { - try { - localStorage.setItem('darkMode', JSON.stringify(darkMode)); - - // Update document class for global styling - if (darkMode) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); - } - } catch (error) { - console.error('Error saving dark mode preference:', error); + // Update theme and save preference + const handleThemeChange = (newTheme) => { + setTheme(newTheme); + + // Save to localStorage + const preferences = getUserPreferences(); + setUserPreferences({ ...preferences, theme: newTheme }); + + // Update document class for global styling + if (newTheme === 'dark') { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); } - }, [darkMode]); + }; + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleChange = (e) => { + const systemTheme = e.matches ? 'dark' : 'light'; + handleThemeChange(systemTheme); + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + // Apply theme on mount and changes + useEffect(() => { + handleThemeChange(theme); + }, [theme]); return ( -
+
- + } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> @@ -89,9 +107,9 @@ function App() { toastOptions={{ duration: 3000, style: { - background: darkMode ? '#374151' : '#ffffff', - color: darkMode ? '#ffffff' : '#000000', - border: `1px solid ${darkMode ? '#4B5563' : '#E5E7EB'}`, + background: theme === 'dark' ? '#374151' : '#ffffff', + color: theme === 'dark' ? '#ffffff' : '#000000', + border: `1px solid ${theme === 'dark' ? '#4B5563' : '#E5E7EB'}`, }, }} /> diff --git a/frontend/src/components/Layout/Footer.jsx b/frontend/src/components/Layout/Footer.jsx index f4c96a3..ba9b9e8 100644 --- a/frontend/src/components/Layout/Footer.jsx +++ b/frontend/src/components/Layout/Footer.jsx @@ -1,6 +1,7 @@ import { FaGithub, FaLinkedin, FaTwitter } from "react-icons/fa"; -const Footer = ({ darkMode }) => { +const Footer = ({ theme }) => { + const isDark = theme === 'dark'; const currentYear = new Date().getFullYear(); const socialLinks = [ @@ -35,7 +36,7 @@ const Footer = ({ darkMode }) => { return (