Skip to content

Commit 8b87ceb

Browse files
authored
Add dark mode & refactor styling (#323)
1 parent 7948328 commit 8b87ceb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+632
-334
lines changed

api/src/controllers/users.ts

+47
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import express, { Request, Response } from 'express';
66
import passport from 'passport';
7+
import { COLLECTION_NAMES, addDocument, containsID, getDocuments, updateDocument } from '../helpers/mongo';
78
import { SESSION_LENGTH } from '../config/constants';
89
import { User } from 'express-session';
910

@@ -16,6 +17,52 @@ router.get('/', function (req, res) {
1617
res.json(req.session);
1718
});
1819

20+
router.get('/preferences', async (req, res) => {
21+
if (!req.session.passport) {
22+
res.json({ error: 'Must be logged in to get preferences.' });
23+
return;
24+
}
25+
26+
const userID = req.session.passport.user.id;
27+
const preferences = await getDocuments(COLLECTION_NAMES.PREFERENCES, { _id: userID });
28+
29+
if (preferences.length > 0) {
30+
res.json(preferences[0]);
31+
} else {
32+
res.json({ error: 'No preferences found' });
33+
}
34+
});
35+
36+
interface UserPreferences {
37+
theme?: 'light' | 'dark' | 'system';
38+
}
39+
40+
router.post('/preferences', async (req, res) => {
41+
if (!req.session.passport) {
42+
res.json({ error: 'Must be logged in to get preferences.' });
43+
return;
44+
}
45+
46+
const userID = req.session.passport.user.id;
47+
48+
// make user's preference doc if it doesn't exist
49+
if (!(await containsID(COLLECTION_NAMES.PREFERENCES, userID))) {
50+
await addDocument(COLLECTION_NAMES.PREFERENCES, { _id: userID });
51+
}
52+
53+
// grab valid preferences from request body
54+
const preferences: UserPreferences = {};
55+
if (req.body.theme) {
56+
preferences.theme = req.body.theme;
57+
}
58+
59+
// set the preferences
60+
await updateDocument(COLLECTION_NAMES.PREFERENCES, { _id: userID }, { $set: preferences });
61+
62+
// echo back body
63+
res.json(req.body);
64+
});
65+
1966
/**
2067
* Get whether or not a user is an admin
2168
*/

api/src/helpers/mongo.ts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const COLLECTION_NAMES = {
2222
ROADMAPS: 'roadmaps',
2323
VOTES: 'votes',
2424
REPORTS: 'reports',
25+
PREFERENCES: 'preferences',
2526
};
2627
/**
2728
* Global reference to database

site/public/working.gif

-1.75 MB
Binary file not shown.

site/src/App.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414
height: 100%;
1515
overflow-y: auto;
1616
overflow-x: hidden;
17-
background-color: var(--peterportal-gray-blue);
17+
background-color: var(--background);
1818
box-sizing: border-box;
1919
}

site/src/App.tsx

+84-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { useCallback, useEffect, useState } from 'react';
12
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
23
import 'semantic-ui-css/semantic.min.css';
34
import 'bootstrap/dist/css/bootstrap.min.css';
45
import 'react-bootstrap-range-slider/dist/react-bootstrap-range-slider.css';
6+
import './style/theme.scss';
57
import './App.scss';
68

79
import AppHeader from './component/AppHeader/AppHeader';
@@ -15,28 +17,92 @@ import AdminPage from './pages/AdminPage';
1517
import ReviewsPage from './pages/ReviewsPage';
1618
import SideBar from './component/SideBar/SideBar';
1719

20+
import ThemeContext, { Theme } from './style/theme-context';
21+
import axios from 'axios';
22+
import { useCookies } from 'react-cookie';
23+
24+
function isSystemDark() {
25+
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
26+
}
27+
1828
export default function App() {
29+
// default darkMode to local or system preferences
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+
);
36+
const [cookies] = useCookies(['user']);
37+
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+
66+
useEffect(() => {
67+
// if logged in, load user prefs (theme) from mongo
68+
if (cookies.user) {
69+
axios.get('/api/users/preferences').then((res) => {
70+
const { theme }: { theme?: Theme } = res.data;
71+
if (theme) {
72+
setThemeState(theme);
73+
}
74+
});
75+
}
76+
}, [cookies.user, setThemeState]);
77+
78+
useEffect(() => {
79+
// Theme styling is controlled by data-theme attribute on body being set to light or dark
80+
document.querySelector('body')!.setAttribute('data-theme', darkMode ? 'dark' : 'light');
81+
}, [darkMode]);
82+
1983
return (
2084
<Router>
21-
<AppHeader />
22-
<div className="app-body">
23-
<div className="app-sidebar">
24-
<SideBar></SideBar>
25-
</div>
26-
<div className="app-content">
27-
<Routes>
28-
<Route path="/roadmap" element={<RoadmapPage />} />
29-
<Route path="/" element={<SearchPage />} />
30-
<Route path="/search/:index" element={<SearchPage />} />
31-
<Route path="/course/:id" element={<CoursePage />} />
32-
<Route path="/professor/:id" element={<ProfessorPage />} />
33-
<Route path="/admin/*" element={<AdminPage />} />
34-
<Route path="/reviews" element={<ReviewsPage />} />
35-
<Route path="*" element={<ErrorPage />} />
36-
</Routes>
37-
<Footer />
85+
<ThemeContext.Provider value={{ darkMode, usingSystemTheme, setTheme }}>
86+
<AppHeader />
87+
<div className="app-body">
88+
<div className="app-sidebar">
89+
<SideBar></SideBar>
90+
</div>
91+
<div className="app-content">
92+
<Routes>
93+
<Route path="/roadmap" element={<RoadmapPage />} />
94+
<Route path="/" element={<SearchPage />} />
95+
<Route path="/search/:index" element={<SearchPage />} />
96+
<Route path="/course/:id" element={<CoursePage />} />
97+
<Route path="/professor/:id" element={<ProfessorPage />} />
98+
<Route path="/admin/*" element={<AdminPage />} />
99+
<Route path="/reviews" element={<ReviewsPage />} />
100+
<Route path="*" element={<ErrorPage />} />
101+
</Routes>
102+
<Footer />
103+
</div>
38104
</div>
39-
</div>
105+
</ThemeContext.Provider>
40106
</Router>
41107
);
42108
}
8.99 KB
Loading

site/src/asset/no-results-crop.webp

66.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)