Skip to content

Commit afc5b84

Browse files
authored
Merge pull request #16 from tarone-saloni/Dark
Dark/Light Theme Toggle Enhancement
2 parents 958b96c + 72f193a commit afc5b84

7 files changed

Lines changed: 186 additions & 88 deletions

File tree

frontend/src/App.css

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,43 @@
1+
:root {
2+
/* Light theme variables */
3+
--bg-primary: #f9fafb;
4+
--bg-secondary: #ffffff;
5+
--text-primary: #111827;
6+
--text-secondary: #4b5563;
7+
--accent-primary: #3b82f6;
8+
--accent-secondary: #60a5fa;
9+
--border-color: #e5e7eb;
10+
--shadow-color: rgba(0, 0, 0, 0.1);
11+
12+
/* Animation durations */
13+
--transition-duration: 300ms;
14+
--transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
15+
}
16+
17+
:root[class~="dark"] {
18+
/* Dark theme variables */
19+
--bg-primary: #111827;
20+
--bg-secondary: #1f2937;
21+
--text-primary: #f9fafb;
22+
--text-secondary: #d1d5db;
23+
--accent-primary: #60a5fa;
24+
--accent-secondary: #93c5fd;
25+
--border-color: #374151;
26+
--shadow-color: rgba(0, 0, 0, 0.25);
27+
}
28+
29+
/* Apply transitions to theme changes */
30+
*, *::before, *::after {
31+
transition: background-color var(--transition-duration) var(--transition-timing),
32+
border-color var(--transition-duration) var(--transition-timing),
33+
color var(--transition-duration) var(--transition-timing),
34+
box-shadow var(--transition-duration) var(--transition-timing);
35+
}
36+
137
.App {
238
min-height: 100vh;
3-
background-color: #f9fafb;
39+
background-color: var(--bg-primary);
40+
color: var(--text-primary);
441
}
542

643
/* Loading spinner */

frontend/src/App.js

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
22
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
33
import { Toaster } from 'react-hot-toast';
44
import Layout from './components/Layout/Layout';
5+
import { getUserPreferences, setUserPreferences } from './services/userPreferences';
56
import SortingVisualizer from './pages/SortingVisualizer';
67
import GraphVisualizer from './pages/GraphVisualizer';
78
import StringVisualizer from './pages/StringVisualizer';
@@ -13,71 +14,88 @@ import ContributorsPage from './pages/ContributorsPage';
1314
import './App.css';
1415

1516
function App() {
16-
// Dark mode state with persistence
17-
const [darkMode, setDarkMode] = useState(() => {
17+
// Theme state with persistence
18+
const [theme, setTheme] = useState(() => {
19+
// Check system preference first
20+
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1821
try {
19-
const saved = localStorage.getItem('darkMode');
20-
return saved ? JSON.parse(saved) : false;
22+
const { theme: savedTheme } = getUserPreferences();
23+
return savedTheme || systemPreference;
2124
} catch (error) {
22-
console.error('Error loading dark mode preference:', error);
23-
return false;
25+
console.error('Error loading theme preference:', error);
26+
return systemPreference;
2427
}
2528
});
2629

27-
// Save dark mode preference
28-
useEffect(() => {
29-
try {
30-
localStorage.setItem('darkMode', JSON.stringify(darkMode));
31-
32-
// Update document class for global styling
33-
if (darkMode) {
34-
document.documentElement.classList.add('dark');
35-
} else {
36-
document.documentElement.classList.remove('dark');
37-
}
38-
} catch (error) {
39-
console.error('Error saving dark mode preference:', error);
30+
// Update theme and save preference
31+
const handleThemeChange = (newTheme) => {
32+
setTheme(newTheme);
33+
34+
// Save to localStorage
35+
const preferences = getUserPreferences();
36+
setUserPreferences({ ...preferences, theme: newTheme });
37+
38+
// Update document class for global styling
39+
if (newTheme === 'dark') {
40+
document.documentElement.classList.add('dark');
41+
} else {
42+
document.documentElement.classList.remove('dark');
4043
}
41-
}, [darkMode]);
44+
};
45+
46+
// Listen for system theme changes
47+
useEffect(() => {
48+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
49+
const handleChange = (e) => {
50+
const systemTheme = e.matches ? 'dark' : 'light';
51+
handleThemeChange(systemTheme);
52+
};
53+
54+
mediaQuery.addEventListener('change', handleChange);
55+
return () => mediaQuery.removeEventListener('change', handleChange);
56+
}, []);
57+
58+
// Apply theme on mount and changes
59+
useEffect(() => {
60+
handleThemeChange(theme);
61+
}, [theme]);
4262

4363
return (
44-
<div className={`min-h-screen transition-colors duration-300 ${
45-
darkMode ? 'dark bg-gray-900' : 'bg-white'
46-
}`}>
64+
<div className="App">
4765
<Router>
48-
<Layout darkMode={darkMode} setDarkMode={setDarkMode}>
66+
<Layout theme={theme} onThemeChange={handleThemeChange}>
4967
<Routes>
5068
<Route
5169
path="/"
52-
element={<HomePage darkMode={darkMode} setDarkMode={setDarkMode} />}
70+
element={<HomePage theme={theme} />}
5371
/>
5472
<Route
5573
path="/sorting"
56-
element={<SortingVisualizer darkMode={darkMode} setDarkMode={setDarkMode} />}
74+
element={<SortingVisualizer theme={theme} />}
5775
/>
5876
<Route
5977
path="/graph"
60-
element={<GraphVisualizer darkMode={darkMode} setDarkMode={setDarkMode} />}
78+
element={<GraphVisualizer theme={theme} />}
6179
/>
6280
<Route
6381
path="/string"
64-
element={<StringVisualizer darkMode={darkMode} setDarkMode={setDarkMode} />}
82+
element={<StringVisualizer theme={theme} />}
6583
/>
6684
<Route
6785
path="/dp"
68-
element={<DPVisualizer darkMode={darkMode} setDarkMode={setDarkMode} />}
86+
element={<DPVisualizer theme={theme} />}
6987
/>
7088
<Route
7189
path="/about"
72-
element={<AboutPage darkMode={darkMode} setDarkMode={setDarkMode} />}
90+
element={<AboutPage theme={theme} />}
7391
/>
7492
<Route
7593
path="/docs"
76-
element={<DocumentationPage darkMode={darkMode} setDarkMode={setDarkMode} />}
94+
element={<DocumentationPage theme={theme} />}
7795
/>
7896
<Route
7997
path="/contributors"
80-
element={<ContributorsPage darkMode={darkMode} setDarkMode={setDarkMode} />}
98+
element={<ContributorsPage theme={theme} />}
8199
/>
82100
</Routes>
83101
</Layout>
@@ -89,9 +107,9 @@ function App() {
89107
toastOptions={{
90108
duration: 3000,
91109
style: {
92-
background: darkMode ? '#374151' : '#ffffff',
93-
color: darkMode ? '#ffffff' : '#000000',
94-
border: `1px solid ${darkMode ? '#4B5563' : '#E5E7EB'}`,
110+
background: theme === 'dark' ? '#374151' : '#ffffff',
111+
color: theme === 'dark' ? '#ffffff' : '#000000',
112+
border: `1px solid ${theme === 'dark' ? '#4B5563' : '#E5E7EB'}`,
95113
},
96114
}}
97115
/>

frontend/src/components/Layout/Footer.jsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FaGithub, FaLinkedin, FaTwitter } from "react-icons/fa";
22

3-
const Footer = ({ darkMode }) => {
3+
const Footer = ({ theme }) => {
4+
const isDark = theme === 'dark';
45
const currentYear = new Date().getFullYear();
56

67
const socialLinks = [
@@ -35,7 +36,7 @@ const Footer = ({ darkMode }) => {
3536
return (
3637
<footer
3738
className={`py-12 transition-all duration-500 ${
38-
darkMode ? "bg-gray-900 text-gray-200" : "bg-gray-50 text-gray-800"
39+
isDark ? "bg-gray-900 text-gray-200" : "bg-gray-50 text-gray-800"
3940
}`}
4041
>
4142
{/* Top Row: Social & Quick Links */}
@@ -70,7 +71,7 @@ const Footer = ({ darkMode }) => {
7071
<div className="flex-1">
7172
<h3
7273
className={`text-lg font-semibold mb-4 ${
73-
darkMode ? "text-white" : "text-gray-900"
74+
isDark ? "text-white" : "text-gray-900"
7475
}`}
7576
>
7677
Quick Links
@@ -92,7 +93,7 @@ const Footer = ({ darkMode }) => {
9293
{/* Divider */}
9394
<div
9495
className={`mt-10 border-t ${
95-
darkMode ? "border-gray-700" : "border-gray-300"
96+
isDark ? "border-gray-700" : "border-gray-300"
9697
}`}
9798
></div>
9899

frontend/src/components/Layout/Layout.jsx

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,12 @@ import React, { useState, useEffect } from "react";
22
import Navbar from "./Navbar";
33
import Footer from "./Footer";
44

5-
const Layout = ({ children }) => {
6-
const [darkMode, setDarkMode] = useState(() => {
7-
// Check localStorage for saved theme preference
8-
const saved = localStorage.getItem("darkMode");
9-
return saved ? JSON.parse(saved) : false;
10-
});
11-
12-
// Save theme preference to localStorage
13-
useEffect(() => {
14-
localStorage.setItem("darkMode", JSON.stringify(darkMode));
15-
}, [darkMode]);
16-
17-
// Apply theme to document
18-
useEffect(() => {
19-
if (darkMode) {
20-
document.documentElement.classList.add("dark");
21-
} else {
22-
document.documentElement.classList.remove("dark");
23-
}
24-
}, [darkMode]);
25-
5+
const Layout = ({ children, theme, onThemeChange }) => {
266
return (
27-
<div className={`min-h-screen ${darkMode ? "dark" : ""}`}>
28-
<Navbar darkMode={darkMode} setDarkMode={setDarkMode} />
7+
<div className="min-h-screen">
8+
<Navbar theme={theme} onThemeChange={onThemeChange} />
299
<main className="pt-16">{children}</main>
30-
<Footer darkMode={darkMode} />
10+
<Footer theme={theme} />
3111
</div>
3212
);
3313
};

frontend/src/components/Layout/Navbar.jsx

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React, { useState, useEffect, useRef } from 'react';
2-
import { Menu, X, ChevronDown, User, Settings, LogOut, Moon, Sun, Zap, BarChart3, Network, Code, BookOpen, Type, Layers, Info } from 'lucide-react';
2+
import { Menu, X, ChevronDown, User, Settings, LogOut, Zap, BarChart3, Network, Code, BookOpen, Type, Layers, Info } from 'lucide-react';
33
import { Link, useLocation } from 'react-router-dom';
4+
import ThemeToggle from './ThemeToggle';
45

5-
const Navbar = ({ darkMode, setDarkMode }) => {
6+
const Navbar = ({ theme, onThemeChange }) => {
7+
const isDark = theme === 'dark';
68
const [isMenuOpen, setIsMenuOpen] = useState(false);
79
const [isProductsOpen, setIsProductsOpen] = useState(false);
810
const [isProfileOpen, setIsProfileOpen] = useState(false);
@@ -83,12 +85,12 @@ const Navbar = ({ darkMode, setDarkMode }) => {
8385
fixed top-0 left-0 right-0 z-50 transition-all duration-200
8486
${isScrolled
8587
? `backdrop-blur-xl border-b ${
86-
darkMode
88+
isDark
8789
? 'bg-gray-900/80 border-gray-800'
8890
: 'bg-white/80 border-gray-200'
8991
}`
9092
: `backdrop-blur-sm ${
91-
darkMode
93+
isDark
9294
? 'bg-gray-900/40 border-gray-800/40'
9395
: 'bg-white/40 border-gray-200/40'
9496
} border-b`
@@ -106,7 +108,7 @@ const Navbar = ({ darkMode, setDarkMode }) => {
106108
transition-colors duration-200 focus-visible:outline-none
107109
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
108110
rounded-lg px-2 py-1
109-
${darkMode
111+
${isDark
110112
? 'text-white hover:text-blue-400 focus-visible:ring-offset-gray-900'
111113
: 'text-gray-900 hover:text-blue-600 focus-visible:ring-offset-white'
112114
}
@@ -134,8 +136,8 @@ const Navbar = ({ darkMode, setDarkMode }) => {
134136
focus-visible:outline-none focus-visible:ring-2
135137
focus-visible:ring-blue-500 focus-visible:ring-offset-2
136138
${isActive(path)
137-
? (darkMode ? 'bg-blue-600 text-white' : 'bg-blue-600 text-white')
138-
: (darkMode
139+
? 'bg-blue-600 text-white'
140+
: (isDark
139141
? 'text-gray-300 hover:text-white hover:bg-gray-700'
140142
: 'text-gray-700 hover:text-gray-900 hover:bg-gray-100'
141143
)
@@ -152,19 +154,7 @@ const Navbar = ({ darkMode, setDarkMode }) => {
152154
{/* Right side items */}
153155
<div className="hidden md:flex items-center space-x-3">
154156
{/* Theme Toggle */}
155-
<button
156-
onClick={() => setDarkMode(!darkMode)}
157-
className={`
158-
p-2 rounded-lg transition-all
159-
${darkMode
160-
? 'bg-yellow-500/20 text-yellow-300 hover:bg-yellow-500/30'
161-
: 'bg-gray-800/20 text-gray-600 hover:bg-gray-800/30'
162-
}
163-
`}
164-
aria-label="Toggle theme"
165-
>
166-
{darkMode ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
167-
</button>
157+
<ThemeToggle theme={theme} onToggle={onThemeChange} />
168158
</div>
169159

170160
{/* Mobile menu button */}
@@ -175,7 +165,7 @@ const Navbar = ({ darkMode, setDarkMode }) => {
175165
p-2 rounded-lg transition-colors duration-200
176166
focus-visible:outline-none focus-visible:ring-2
177167
focus-visible:ring-blue-500 focus-visible:ring-offset-2
178-
${darkMode
168+
${isDark
179169
? 'hover:bg-gray-800 text-gray-300 hover:text-white focus-visible:ring-offset-gray-900'
180170
: 'hover:bg-gray-100 text-gray-600 hover:text-gray-900 focus-visible:ring-offset-white'
181171
}
@@ -191,7 +181,7 @@ const Navbar = ({ darkMode, setDarkMode }) => {
191181
{isMenuOpen && (
192182
<div className={`
193183
md:hidden border-t backdrop-blur-xl
194-
${darkMode ? 'bg-gray-900/95 border-gray-800' : 'bg-white/95 border-gray-200'}
184+
${isDark ? 'bg-gray-900/95 border-gray-800' : 'bg-white/95 border-gray-200'}
195185
`}>
196186
<div className="px-4 py-3 space-y-1">
197187
{navItems.map(({ path, label, icon: Icon }) => (
@@ -203,7 +193,7 @@ const Navbar = ({ darkMode, setDarkMode }) => {
203193
transition-colors duration-200
204194
focus-visible:outline-none focus-visible:ring-2
205195
focus-visible:ring-blue-500 focus-visible:ring-offset-2
206-
${darkMode
196+
${isDark
207197
? `hover:bg-gray-800 focus-visible:ring-offset-gray-900
208198
${isActive(path) ? 'bg-gray-800 text-white' : 'text-gray-300'}`
209199
: `hover:bg-gray-100 focus-visible:ring-offset-white
@@ -227,7 +217,7 @@ const Navbar = ({ darkMode, setDarkMode }) => {
227217
transition-all duration-200
228218
focus-visible:outline-none focus-visible:ring-2
229219
focus-visible:ring-blue-500 focus-visible:ring-offset-2
230-
${darkMode ? 'focus-visible:ring-offset-gray-900' : 'focus-visible:ring-offset-white'}
220+
${isDark ? 'focus-visible:ring-offset-gray-900' : 'focus-visible:ring-offset-white'}
231221
`}
232222
>
233223
Get Started

0 commit comments

Comments
 (0)