From 737188d0b0b76a2d455ebc4bf8adbebebb566e5e Mon Sep 17 00:00:00 2001 From: MarioLJFerreira Date: Sat, 16 Aug 2025 01:58:55 +0100 Subject: [PATCH] feat: add dark mode toggle with localStorage persistence --- src/components/layout/Header.jsx | 2 ++ src/components/ui/Button.jsx | 8 +++++- src/components/ui/ThemeToggle.jsx | 42 +++++++++++++++++++++++++++ src/context/ThemeContext.jsx | 24 ++++++++++++++-- src/hooks/useDarkMode.js | 48 +++++++++++++++++++++++++++++-- src/index.css | 42 ++++++++++++++++----------- src/main.jsx | 9 ++++-- src/styles/layout.module.css | 7 ++--- 8 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 src/components/ui/ThemeToggle.jsx diff --git a/src/components/layout/Header.jsx b/src/components/layout/Header.jsx index e106937..a6bd657 100644 --- a/src/components/layout/Header.jsx +++ b/src/components/layout/Header.jsx @@ -6,6 +6,7 @@ import { doSignOut } from '/src/firebase/auth' import { useState } from 'react' import styles from '../../styles/layout.module.css' import logo from '../../assets/logo.svg' +import { DarkModeButton } from '../ui/ThemeToggle.jsx' export default function Header() { const [isSignInOpen, setIsSignInOpen] = useState(false) @@ -30,6 +31,7 @@ export default function Header() { DragMe Logo
+ {!userLoggedIn ? ( <> diff --git a/src/components/ui/Button.jsx b/src/components/ui/Button.jsx index e908902..2925160 100644 --- a/src/components/ui/Button.jsx +++ b/src/components/ui/Button.jsx @@ -12,5 +12,11 @@ function SignInButton({ onClick }) { ) } + + function DarkModeButton({ onClick }) { + return ( + + ) + } - export { SignInButton, SignUpButton } \ No newline at end of file + export { SignInButton, SignUpButton, DarkModeButton } \ No newline at end of file diff --git a/src/components/ui/ThemeToggle.jsx b/src/components/ui/ThemeToggle.jsx new file mode 100644 index 0000000..35cf150 --- /dev/null +++ b/src/components/ui/ThemeToggle.jsx @@ -0,0 +1,42 @@ +import { useTheme } from '../../context/ThemeContext'; + +export function DarkModeButton() { + const { isDarkMode, toggleDarkMode } = useTheme(); + + return ( +
+
+ {isDarkMode ? '🌙' : '☀️'} +
+
+ ); +} diff --git a/src/context/ThemeContext.jsx b/src/context/ThemeContext.jsx index b236fa8..382ab4e 100644 --- a/src/context/ThemeContext.jsx +++ b/src/context/ThemeContext.jsx @@ -1,2 +1,22 @@ -// ThemeContext - Manages the light/dark mode theme -// TODO: Implement theme context provider \ No newline at end of file +import { createContext, useContext } from 'react'; +import { useDarkMode } from '../hooks/useDarkMode'; + +const ThemeContext = createContext(); + +export function ThemeProvider({ children }) { + const { isDarkMode, toggleDarkMode } = useDarkMode(); + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/hooks/useDarkMode.js b/src/hooks/useDarkMode.js index 2f992e2..c106f36 100644 --- a/src/hooks/useDarkMode.js +++ b/src/hooks/useDarkMode.js @@ -1,2 +1,46 @@ -// useDarkMode hook - Manages dark mode state and local storage -// TODO: Implement dark mode hook with theme switching \ No newline at end of file +import { useState, useEffect } from 'react'; + +export function useDarkMode() { + // Initialize state from localStorage or default to system preference + const [isDarkMode, setIsDarkMode] = useState(() => { + const saved = localStorage.getItem('darkMode'); + if (saved !== null) { + return JSON.parse(saved); + } + // Default to system preference if no saved preference + return window.matchMedia('(prefers-color-scheme: dark)').matches; + }); + + // Update localStorage and document class when theme changes + useEffect(() => { + localStorage.setItem('darkMode', JSON.stringify(isDarkMode)); + + // Add or remove 'dark' class to document for CSS targeting + if (isDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [isDarkMode]); + + // Listen for system theme changes + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = (e) => { + // Only update if user hasn't set a preference + if (localStorage.getItem('darkMode') === null) { + setIsDarkMode(e.matches); + } + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, []); + + const toggleDarkMode = () => { + setIsDarkMode(prev => !prev); + }; + + return { isDarkMode, toggleDarkMode }; +} diff --git a/src/index.css b/src/index.css index 5c137dc..c9e207a 100644 --- a/src/index.css +++ b/src/index.css @@ -1,9 +1,26 @@ @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'); + :root { + --text: #1a1a1a; + --background: #ffffff; + --buttonBackground: #f9f9f9; + --buttonBorder: #202020; + --inputBackground: #f9f9f9; + --inputBorder: #202020; + --googleButtonBackground: #f9f9f9; + --cardBackground: white; + --cardSpanBackground: white; + --spanTextColor: #6b7280; + --dividerColor: #e5e7eb; + font-family: 'Roboto', sans-serif; +} + +/* Dark mode styles */ +.dark { --text: #f9f9f9; --background: #202020; --buttonBackground: #1a1a1a; - --buttonBorder: #d1d5db;; + --buttonBorder: #d1d5db; --inputBackground: #1a1a1a; --inputBorder: #202020; --googleButtonBackground: #1a1a1a; @@ -11,11 +28,12 @@ --cardSpanBackground: #1a1a1a; --spanTextColor: #f9f9f9; --dividerColor: #2a2b2d; - font-family: 'Roboto', sans-serif; } -body{ + +body { color: var(--text); background-color: var(--background); + transition: color 0.3s ease, background-color 0.3s ease; } button { @@ -27,21 +45,11 @@ button { font-weight: 500; font-family: inherit; background-color: var(--buttonBackground); - border-color: var(--borderColor); + border-color: var(--buttonBorder); cursor: pointer; + transition: all 0.3s ease; } -@media (prefers-color-scheme: light) { - :root { - --text: #1a1a1a; - --buttonBorder: #202020; - --background: #ffffff; - --buttonBackground: #f9f9f9; - --inputBackground: #f9f9f9; - --cardSpanBackground: white; - --cardBackground: white; - --googleButtonBackground: #f9f9f9; - --spanTextColor: #6b7280; - --dividerColor: #e5e7eb; - } +button:hover { + opacity: 0.8; } diff --git a/src/main.jsx b/src/main.jsx index ec394e1..6cdbf40 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' import { AuthProvider } from './context/AuthContext' +import { ThemeProvider } from './context/ThemeContext' createRoot(document.getElementById('root')).render( - - - + + + + + , ) diff --git a/src/styles/layout.module.css b/src/styles/layout.module.css index 539a4a6..b8377bd 100644 --- a/src/styles/layout.module.css +++ b/src/styles/layout.module.css @@ -31,8 +31,7 @@ flex-shrink: 0; } -@media (prefers-color-scheme: dark) { - .logo { - filter: brightness(0) invert(1); - } +/* Dark mode logo styling */ +:global(.dark) .logo { + filter: brightness(0) invert(1); } \ No newline at end of file