-
Notifications
You must be signed in to change notification settings - Fork 35
feat: Add dark mode support with toggle button in navbar #362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,14 @@ | ||
| import React, { useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { Link, useNavigate } from 'react-router-dom'; | ||
| import { Menu, User, LogOut } from 'lucide-react'; | ||
| import { Menu, User, LogOut, Sun, Moon } from 'lucide-react'; | ||
| import { useAuth } from '../contexts/AuthContext'; | ||
| import { useDarkMode } from '../contexts/DarkModeContext'; | ||
|
|
||
| const AppHeader = () => { | ||
| const navigate = useNavigate(); | ||
| const { user, logout } = useAuth(); // useAuth returns user, not currentUser | ||
| const { isDarkMode, toggleDarkMode } = useDarkMode(); | ||
| const [isMenuOpen, setIsMenuOpen] = useState(false); | ||
|
|
||
| const handleLogout = async () => { | ||
|
|
@@ -19,7 +21,7 @@ const AppHeader = () => { | |
| }; | ||
|
|
||
| return ( | ||
| <header className="bg-white shadow-sm sticky top-0 z-40"> | ||
| <header className="bg-white dark:bg-gray-900 shadow-sm sticky top-0 z-40 transition-colors duration-300"> | ||
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | ||
| <div className="flex justify-between h-16 items-center"> | ||
| <div className="flex items-center cursor-pointer" onClick={() => navigate('/')}> | ||
|
|
@@ -29,26 +31,39 @@ const AppHeader = () => { | |
| </div> | ||
|
|
||
| <div className="flex items-center gap-4"> | ||
| {/* Dark Mode Toggle Button */} | ||
| <button | ||
| onClick={toggleDarkMode} | ||
| className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200" | ||
| title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'} | ||
| > | ||
| {isDarkMode ? ( | ||
| <Sun size={20} className="text-yellow-400" /> | ||
| ) : ( | ||
| <Moon size={20} className="text-gray-700" /> | ||
| )} | ||
| </button> | ||
|
Comment on lines
+35
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add The toggle button uses Proposed fix <button
onClick={toggleDarkMode}
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
+ aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>π€ Prompt for AI Agents |
||
|
|
||
| {user ? ( | ||
| <div className="relative"> | ||
| <button onClick={() => setIsMenuOpen(!isMenuOpen)} className="flex items-center gap-2 text-gray-700 hover:text-blue-600 focus:outline-none"> | ||
| <div className="bg-blue-100 p-2 rounded-full"> | ||
| <User size={20} className="text-blue-600" /> | ||
| <button onClick={() => setIsMenuOpen(!isMenuOpen)} className="flex items-center gap-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors duration-200"> | ||
| <div className="bg-blue-100 dark:bg-blue-900 p-2 rounded-full"> | ||
| <User size={20} className="text-blue-600 dark:text-blue-400" /> | ||
| </div> | ||
| <span className="hidden sm:inline font-medium text-sm">{user.email}</span> | ||
| </button> | ||
|
|
||
| {isMenuOpen && ( | ||
| <div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50"> | ||
| <Link to="/my-reports" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => setIsMenuOpen(false)}>My Reports</Link> | ||
| <button onClick={handleLogout} className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"> | ||
| <div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50"> | ||
| <Link to="/my-reports" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200" onClick={() => setIsMenuOpen(false)}>My Reports</Link> | ||
| <button onClick={handleLogout} className="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 transition-colors duration-200"> | ||
| <LogOut size={14} /> Logout | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) : ( | ||
| <Link to="/login" className="text-sm font-medium text-blue-600 hover:text-blue-500 px-4 py-2 rounded-full hover:bg-blue-50 transition-colors">Login</Link> | ||
| <Link to="/login" className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 px-4 py-2 rounded-full hover:bg-blue-50 dark:hover:bg-gray-800 transition-colors duration-200">Login</Link> | ||
| )} | ||
| </div> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,44 @@ | ||||||||||||||||||||||||||||||||||||||||||
| import React, { createContext, useContext, useState, useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const DarkModeContext = createContext(null); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const DarkModeProvider = ({ children }) => { | ||||||||||||||||||||||||||||||||||||||||||
| const [isDarkMode, setIsDarkMode] = useState(() => { | ||||||||||||||||||||||||||||||||||||||||||
| // Initialize from localStorage, default to light mode (false) | ||||||||||||||||||||||||||||||||||||||||||
| const savedMode = localStorage.getItem('darkMode'); | ||||||||||||||||||||||||||||||||||||||||||
| if (savedMode !== null) { | ||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Guard JSON.parse when reading Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||||||
| return JSON.parse(savedMode); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| // Default to light mode | ||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing system preference detection ( The PR objectives and linked issue Proposed fix const [isDarkMode, setIsDarkMode] = useState(() => {
const savedMode = localStorage.getItem('darkMode');
if (savedMode !== null) {
- return JSON.parse(savedMode);
+ try {
+ return JSON.parse(savedMode);
+ } catch {
+ return false;
+ }
}
- // Default to light mode
- return false;
+ // Respect system preference
+ return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
});π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Apply dark mode class to document root | ||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||
| const htmlElement = document.documentElement; | ||||||||||||||||||||||||||||||||||||||||||
| if (isDarkMode) { | ||||||||||||||||||||||||||||||||||||||||||
| htmlElement.classList.add('dark'); | ||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||
| htmlElement.classList.remove('dark'); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem('darkMode', JSON.stringify(isDarkMode)); | ||||||||||||||||||||||||||||||||||||||||||
| }, [isDarkMode]); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const toggleDarkMode = () => { | ||||||||||||||||||||||||||||||||||||||||||
| setIsDarkMode(prev => !prev); | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||
| <DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}> | ||||||||||||||||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||||||||||||||||
| </DarkModeContext.Provider> | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export const useDarkMode = () => { | ||||||||||||||||||||||||||||||||||||||||||
| const context = useContext(DarkModeContext); | ||||||||||||||||||||||||||||||||||||||||||
| if (!context) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error('useDarkMode must be used within DarkModeProvider'); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return context; | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Remove the unused
isDarkModedestructuring or use it; leaving it unused will fail linting and adds dead code.Prompt for AI agents