diff --git a/src/components/App.tsx b/src/components/App.tsx index 76c41f6..69d4d36 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,9 +1,9 @@ import { BrowserRouter, useRoutes } from 'react-router-dom'; import routesConfig from '../config/routesConfig'; import Navbar from './Navbar'; -import { Suspense, useMemo, useState } from 'react'; +import { Suspense, useState, useEffect } from 'react'; import Loading from './Loading'; -import { CssBaseline, ThemeProvider } from '@mui/material'; +import { CssBaseline, Theme, ThemeProvider } from '@mui/material'; import { CustomSnackBarProvider } from '../contexts/CustomSnackBarContext'; import { SnackbarProvider } from 'notistack'; import { tools } from '../tools'; @@ -11,6 +11,8 @@ import './index.css'; import { darkTheme, lightTheme } from '../config/muiConfig'; import ScrollToTopButton from './ScrollToTopButton'; +export type Mode = 'dark' | 'light' | 'system'; + const AppRoutes = () => { const updatedRoutesConfig = [...routesConfig]; tools.forEach((tool) => { @@ -20,10 +22,26 @@ const AppRoutes = () => { }; function App() { - const [darkMode, setDarkMode] = useState(() => { - return localStorage.getItem('theme') === 'dark'; - }); - const theme = useMemo(() => (darkMode ? darkTheme : lightTheme), [darkMode]); + const [mode, setMode] = useState( + () => (localStorage.getItem('theme') || 'system') as Mode + ); + const [theme, setTheme] = useState(() => getTheme(mode)); + useEffect(() => setTheme(getTheme(mode)), [mode]); + + // Make sure to update the theme when the mode changes + useEffect(() => { + const systemDarkModeQuery = window.matchMedia( + '(prefers-color-scheme: dark)' + ); + const handleThemeChange = (e: MediaQueryListEvent) => { + setTheme(e.matches ? darkTheme : lightTheme); + }; + systemDarkModeQuery.addEventListener('change', handleThemeChange); + + return () => { + systemDarkModeQuery.removeEventListener('change', handleThemeChange); + }; + }, []); return ( @@ -38,9 +56,10 @@ function App() { { - setDarkMode((prevState) => !prevState); - localStorage.setItem('theme', darkMode ? 'light' : 'dark'); + mode={mode} + onChangeMode={() => { + setMode((prev) => nextMode(prev)); + localStorage.setItem('theme', nextMode(mode)); }} /> }> @@ -54,4 +73,21 @@ function App() { ); } +function getTheme(mode: Mode): Theme { + switch (mode) { + case 'dark': + return darkTheme; + case 'light': + return lightTheme; + default: + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? darkTheme + : lightTheme; + } +} + +function nextMode(mode: Mode): Mode { + return mode === 'light' ? 'dark' : mode === 'dark' ? 'system' : 'light'; +} + export default App; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index c99de89..e61b10d 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -17,13 +17,17 @@ import { import useMediaQuery from '@mui/material/useMediaQuery'; import { useTheme } from '@mui/material/styles'; import { Icon } from '@iconify/react'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; +import { Mode } from 'components/App'; interface NavbarProps { - onSwitchTheme: () => void; + mode: Mode; + onChangeMode: () => void; } -const Navbar: React.FC = ({ onSwitchTheme }) => { +const Navbar: React.FC = ({ + mode, + onChangeMode: onChangeMode +}) => { const navigate = useNavigate(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); @@ -37,7 +41,19 @@ const Navbar: React.FC = ({ onSwitchTheme }) => { ]; const buttons: ReactNode[] = [ - , + , window.open('https://discord.gg/SDbbn3hT4b', '_blank')} style={{ cursor: 'pointer' }}