Skip to content

Commit 0c63c43

Browse files
committed
add future support for setting to system theme
1 parent 4f98781 commit 0c63c43

File tree

4 files changed

+57
-34
lines changed

4 files changed

+57
-34
lines changed

api/src/controllers/users.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ router.get('/preferences', async (req, res) => {
3333
});
3434

3535
interface UserPreferences {
36-
theme?: string;
36+
theme?: 'light' | 'dark' | 'system';
3737
}
3838

3939
router.post('/preferences', async (req, res) => {

site/src/App.tsx

+45-29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useState } from 'react';
22
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
33
import 'semantic-ui-css/semantic.min.css';
44
import 'bootstrap/dist/css/bootstrap.min.css';
@@ -17,56 +17,72 @@ import AdminPage from './pages/AdminPage';
1717
import ReviewsPage from './pages/ReviewsPage';
1818
import SideBar from './component/SideBar/SideBar';
1919

20-
import ThemeContext from './style/theme-context';
20+
import ThemeContext, { Theme } from './style/theme-context';
2121
import axios from 'axios';
2222
import { useCookies } from 'react-cookie';
2323

24-
function getDefaultDarkModeValue() {
25-
switch (localStorage.getItem('theme')) {
26-
case 'dark':
27-
return true;
28-
case 'light':
29-
return false;
30-
default:
31-
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
32-
}
24+
function isSystemDark() {
25+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
3326
}
3427

3528
export default function App() {
3629
// default darkMode to local or system preferences
37-
const [darkMode, setDarkMode] = useState(getDefaultDarkModeValue());
30+
const [usingSystemTheme, setUsingSystemTheme] = useState(
31+
localStorage.getItem('theme') === 'system' || !localStorage.getItem('theme'),
32+
);
33+
const [darkMode, setDarkMode] = useState(
34+
usingSystemTheme ? isSystemDark() : localStorage.getItem('theme') === 'dark',
35+
);
3836
const [cookies] = useCookies(['user']);
3937

38+
/**
39+
* Sets the theme state
40+
* @param theme
41+
*/
42+
const setThemeState = useCallback((theme: Theme) => {
43+
if (theme === 'system') {
44+
setDarkMode(isSystemDark());
45+
setUsingSystemTheme(true);
46+
} else {
47+
setDarkMode(theme === 'dark');
48+
setUsingSystemTheme(false);
49+
}
50+
}, []);
51+
52+
/**
53+
* Sets the theme state and saves the users theme preference.
54+
* Saves to account if logged in, local storage if not
55+
* @param theme
56+
*/
57+
const setTheme = (theme: Theme) => {
58+
setThemeState(theme);
59+
if (cookies.user) {
60+
axios.post('/api/users/preferences', { theme });
61+
} else {
62+
localStorage.setItem('theme', theme);
63+
}
64+
};
65+
4066
useEffect(() => {
4167
// if logged in, load user prefs (theme) from mongo
4268
if (cookies.user) {
4369
axios.get('/api/users/preferences').then((res) => {
44-
const { theme } = res.data;
45-
if (theme === 'dark') {
46-
setDarkMode(true);
47-
} else if (theme === 'light') {
48-
setDarkMode(false);
70+
const { theme }: { theme?: Theme } = res.data;
71+
if (theme) {
72+
setThemeState(theme);
4973
}
5074
});
5175
}
52-
}, [cookies.user]);
76+
}, [cookies.user, setThemeState]);
5377

5478
useEffect(() => {
79+
// Theme styling is controlled by data-theme attribute on body being set to light or dark
5580
document.querySelector('body')!.setAttribute('data-theme', darkMode ? 'dark' : 'light');
56-
if (cookies.user) {
57-
axios.post('/api/users/preferences', { theme: darkMode ? 'dark' : 'light' });
58-
} else {
59-
localStorage.setItem('theme', darkMode ? 'dark' : 'light');
60-
}
61-
}, [cookies.user, darkMode]);
62-
63-
const toggleTheme = () => {
64-
setDarkMode(!darkMode);
65-
};
81+
}, [darkMode]);
6682

6783
return (
6884
<Router>
69-
<ThemeContext.Provider value={{ darkMode: darkMode, toggleTheme: toggleTheme }}>
85+
<ThemeContext.Provider value={{ darkMode, usingSystemTheme, setTheme }}>
7086
<AppHeader />
7187
<div className="app-body">
7288
<div className="app-sidebar">

site/src/component/SideBar/SideBar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const SideBar = () => {
1919
const [name, setName] = useState('');
2020
const [picture, setPicture] = useState('');
2121
const [isAdmin, setIsAdmin] = useState<boolean>(false);
22-
const { darkMode, toggleTheme } = useContext(ThemeContext);
22+
const { darkMode, setTheme } = useContext(ThemeContext);
2323

2424
const isLoggedIn = cookies.user !== undefined;
2525

@@ -89,7 +89,7 @@ const SideBar = () => {
8989
)}
9090
{showSidebar && (
9191
<li>
92-
<a className="theme-toggle" onClick={toggleTheme}>
92+
<a className="theme-toggle" onClick={() => setTheme(darkMode ? 'light' : 'dark')}>
9393
<div>
9494
<Icon name={darkMode ? 'moon outline' : 'sun outline'} size="large" />
9595
</div>

site/src/style/theme-context.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import React from 'react';
22

3-
const ThemeContext = React.createContext<{ darkMode: boolean; toggleTheme: () => void }>({
3+
export type Theme = 'dark' | 'light' | 'system';
4+
5+
const ThemeContext = React.createContext<{
6+
darkMode: boolean;
7+
usingSystemTheme: boolean;
8+
setTheme: (theme: Theme) => void;
9+
}>({
410
darkMode: false,
5-
toggleTheme: () => {},
11+
usingSystemTheme: false,
12+
setTheme: () => {},
613
});
714

815
export default ThemeContext;

0 commit comments

Comments
 (0)