From 54f9a7a32d74696deb4be8acaa2ea2848880beff Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 02:43:40 +0800 Subject: [PATCH 01/17] Add typings for default display mode --- website/src/types/settings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/types/settings.ts b/website/src/types/settings.ts index 3de27e2448..9a4d47c4be 100644 --- a/website/src/types/settings.ts +++ b/website/src/types/settings.ts @@ -5,6 +5,8 @@ export type Theme = { readonly name: string; }; -export type Mode = 'LIGHT' | 'DARK'; +export type Mode = 'DEFAULT' | 'LIGHT' | 'DARK'; + +export const DEFAULT_MODE: Mode = 'DEFAULT'; export const LIGHT_MODE: Mode = 'LIGHT'; export const DARK_MODE: Mode = 'DARK'; From 7ba7ee85cba6e44f4a41194fe61dd9aa01f833f2 Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 02:43:58 +0800 Subject: [PATCH 02/17] Add default mode to dark mode UI selector --- website/src/views/settings/ModeSelect.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/views/settings/ModeSelect.tsx b/website/src/views/settings/ModeSelect.tsx index 301ce04c52..9d1ee2d884 100644 --- a/website/src/views/settings/ModeSelect.tsx +++ b/website/src/views/settings/ModeSelect.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import classnames from 'classnames'; -import { Mode, LIGHT_MODE, DARK_MODE } from 'types/settings'; +import { Mode, DEFAULT_MODE, LIGHT_MODE, DARK_MODE } from 'types/settings'; type Props = { mode: Mode; @@ -10,6 +10,10 @@ type Props = { type ModeOption = { value: Mode; label: string }; const MODES: ModeOption[] = [ + { + label: 'OS Default', + value: DEFAULT_MODE, + }, { label: 'On', value: DARK_MODE, From 8879634f3337781121a0982c7e1210168544fdc5 Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 02:58:24 +0800 Subject: [PATCH 03/17] Add CSS util to detect OS display mode --- website/src/utils/css.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/src/utils/css.ts b/website/src/utils/css.ts index 209cb808e3..c2cec55fed 100644 --- a/website/src/utils/css.ts +++ b/website/src/utils/css.ts @@ -36,3 +36,9 @@ export function supportsCSSVariables() { // Safari does not support supports('--var', 'red') return CSS.supports && CSS.supports('(--var: red)'); } + +export function getOSPrefersDarkColorScheme() { + const userPrefersDark = + window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return userPrefersDark; +} From 5434adec6d6aba6a513f2e99a06ca7ac8ec2b679 Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 03:00:08 +0800 Subject: [PATCH 04/17] Add OS display mode detection for AppShell --- website/src/views/AppShell.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/src/views/AppShell.tsx b/website/src/views/AppShell.tsx index 6b032ef0fc..1e1abb97a6 100644 --- a/website/src/views/AppShell.tsx +++ b/website/src/views/AppShell.tsx @@ -1,13 +1,13 @@ import { FC, useCallback, useEffect, useState } from 'react'; +import type { SemTimetableConfig } from 'types/timetables'; +import type { Semester } from 'types/modules'; +import { DARK_MODE, DEFAULT_MODE } from 'types/settings'; import { Helmet } from 'react-helmet'; import { NavLink, useHistory } from 'react-router-dom'; import { useDispatch, useSelector, useStore } from 'react-redux'; import classnames from 'classnames'; import { each } from 'lodash'; -import { DARK_MODE } from 'types/settings'; -import type { Semester } from 'types/modules'; -import type { SemTimetableConfig } from 'types/timetables'; import weekText from 'utils/weekText'; import { captureException } from 'utils/error'; @@ -30,6 +30,7 @@ import Logo from 'img/nusmods-logo.svg'; import type { Dispatch } from 'types/redux'; import type { State } from 'types/state'; import type { Actions } from 'types/actions'; +import { getOSPrefersDarkColorScheme } from 'utils/css'; import LoadingSpinner from './components/LoadingSpinner'; import FeedbackModal from './components/FeedbackModal'; import KeyboardShortcuts from './components/KeyboardShortcuts'; @@ -114,7 +115,8 @@ const AppShell: FC = ({ children }) => { const isModuleListReady = moduleList.length; const mode = useSelector((state: State) => state.settings.mode); - const isDarkMode = mode === DARK_MODE; + const osPrefersDarkColorScheme = getOSPrefersDarkColorScheme(); + const isDarkMode = mode === DARK_MODE || (mode === DEFAULT_MODE && osPrefersDarkColorScheme); const theme = useSelector((state: State) => state.theme.id); From 5a98f321b847d484f06e0ab974a51a8b009b43a6 Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 03:16:00 +0800 Subject: [PATCH 05/17] Update defaults and update reducers --- website/src/reducers/settings.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/website/src/reducers/settings.ts b/website/src/reducers/settings.ts index 79dccacdbd..7da0ef7feb 100644 --- a/website/src/reducers/settings.ts +++ b/website/src/reducers/settings.ts @@ -20,9 +20,10 @@ import { } from 'actions/settings'; import { SET_EXPORTED_DATA } from 'actions/constants'; import { DIMENSIONS, withTracker } from 'bootstrapping/matomo'; -import { DARK_MODE, LIGHT_MODE } from 'types/settings'; +import { DEFAULT_MODE, DARK_MODE, LIGHT_MODE } from 'types/settings'; import config from 'config'; import { isRoundDismissed } from 'selectors/modreg'; +import { getOSPrefersDarkColorScheme } from 'utils/css'; export const defaultModRegNotificationState = { semesterKey: config.getSemesterKey(), @@ -34,7 +35,7 @@ export const defaultModRegNotificationState = { const defaultSettingsState: SettingsState = { newStudent: false, faculty: '', - mode: LIGHT_MODE, + mode: DEFAULT_MODE, hiddenInTimetable: [], modRegNotification: defaultModRegNotificationState, moduleTableOrder: 'exam', @@ -62,7 +63,10 @@ function settings(state: SettingsState = defaultSettingsState, action: Actions): case TOGGLE_MODE: return { ...state, - mode: state.mode === LIGHT_MODE ? DARK_MODE : LIGHT_MODE, + mode: + state.mode === DARK_MODE || (state.mode === DEFAULT_MODE && getOSPrefersDarkColorScheme()) + ? LIGHT_MODE + : DARK_MODE, }; case TOGGLE_MODREG_NOTIFICATION_GLOBALLY: From 83b0d35642a421b3be2cdd3eebee3e8ee4fed2e6 Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 03:18:48 +0800 Subject: [PATCH 06/17] Delay evaluation of matchMedia --- website/src/views/AppShell.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/views/AppShell.tsx b/website/src/views/AppShell.tsx index 1e1abb97a6..38430f4664 100644 --- a/website/src/views/AppShell.tsx +++ b/website/src/views/AppShell.tsx @@ -115,8 +115,7 @@ const AppShell: FC = ({ children }) => { const isModuleListReady = moduleList.length; const mode = useSelector((state: State) => state.settings.mode); - const osPrefersDarkColorScheme = getOSPrefersDarkColorScheme(); - const isDarkMode = mode === DARK_MODE || (mode === DEFAULT_MODE && osPrefersDarkColorScheme); + const isDarkMode = mode === DARK_MODE || (mode === DEFAULT_MODE && getOSPrefersDarkColorScheme()); const theme = useSelector((state: State) => state.theme.id); From 72a25cb9f6dc0cec104a85fd3ecc4a67ee63e051 Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 10:50:49 +0800 Subject: [PATCH 07/17] Add comments --- website/src/utils/css.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/src/utils/css.ts b/website/src/utils/css.ts index c2cec55fed..e9c5693b7d 100644 --- a/website/src/utils/css.ts +++ b/website/src/utils/css.ts @@ -38,6 +38,9 @@ export function supportsCSSVariables() { } export function getOSPrefersDarkColorScheme() { + // If the user uses a legacy browser that doesn't support matchMedia or + // doesn't support the prefers-color-scheme media query, this function returns + // false and AppShell will default to light mode const userPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; return userPrefersDark; From 9e313522e9b09c10411796562af2491f6c0a0a3d Mon Sep 17 00:00:00 2001 From: Christopher Goh Date: Sat, 16 Jan 2021 10:53:12 +0800 Subject: [PATCH 08/17] OS Default -> Auto-detect --- website/src/views/settings/ModeSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/views/settings/ModeSelect.tsx b/website/src/views/settings/ModeSelect.tsx index 9d1ee2d884..22a23ad995 100644 --- a/website/src/views/settings/ModeSelect.tsx +++ b/website/src/views/settings/ModeSelect.tsx @@ -11,7 +11,7 @@ type ModeOption = { value: Mode; label: string }; const MODES: ModeOption[] = [ { - label: 'OS Default', + label: 'Auto-detect', value: DEFAULT_MODE, }, { From 7aac77c151b4e614eba2c9d71f5f8beb31103d86 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 15:02:45 +0800 Subject: [PATCH 09/17] feat: undo media query in reducer --- website/src/reducers/settings.ts | 9 ++------- website/src/types/settings.ts | 4 ++-- website/src/utils/css.ts | 9 --------- website/src/views/AppShell.tsx | 4 ++-- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/website/src/reducers/settings.ts b/website/src/reducers/settings.ts index 7da0ef7feb..c3c7ad7d4d 100644 --- a/website/src/reducers/settings.ts +++ b/website/src/reducers/settings.ts @@ -20,10 +20,9 @@ import { } from 'actions/settings'; import { SET_EXPORTED_DATA } from 'actions/constants'; import { DIMENSIONS, withTracker } from 'bootstrapping/matomo'; -import { DEFAULT_MODE, DARK_MODE, LIGHT_MODE } from 'types/settings'; +import { SYSTEM_MODE } from 'types/settings'; import config from 'config'; import { isRoundDismissed } from 'selectors/modreg'; -import { getOSPrefersDarkColorScheme } from 'utils/css'; export const defaultModRegNotificationState = { semesterKey: config.getSemesterKey(), @@ -35,7 +34,7 @@ export const defaultModRegNotificationState = { const defaultSettingsState: SettingsState = { newStudent: false, faculty: '', - mode: DEFAULT_MODE, + mode: SYSTEM_MODE, hiddenInTimetable: [], modRegNotification: defaultModRegNotificationState, moduleTableOrder: 'exam', @@ -63,10 +62,6 @@ function settings(state: SettingsState = defaultSettingsState, action: Actions): case TOGGLE_MODE: return { ...state, - mode: - state.mode === DARK_MODE || (state.mode === DEFAULT_MODE && getOSPrefersDarkColorScheme()) - ? LIGHT_MODE - : DARK_MODE, }; case TOGGLE_MODREG_NOTIFICATION_GLOBALLY: diff --git a/website/src/types/settings.ts b/website/src/types/settings.ts index 9a4d47c4be..859e5a6fa2 100644 --- a/website/src/types/settings.ts +++ b/website/src/types/settings.ts @@ -5,8 +5,8 @@ export type Theme = { readonly name: string; }; -export type Mode = 'DEFAULT' | 'LIGHT' | 'DARK'; +export type Mode = 'SYSTEM' | 'LIGHT' | 'DARK'; -export const DEFAULT_MODE: Mode = 'DEFAULT'; +export const SYSTEM_MODE: Mode = 'SYSTEM'; export const LIGHT_MODE: Mode = 'LIGHT'; export const DARK_MODE: Mode = 'DARK'; diff --git a/website/src/utils/css.ts b/website/src/utils/css.ts index e9c5693b7d..209cb808e3 100644 --- a/website/src/utils/css.ts +++ b/website/src/utils/css.ts @@ -36,12 +36,3 @@ export function supportsCSSVariables() { // Safari does not support supports('--var', 'red') return CSS.supports && CSS.supports('(--var: red)'); } - -export function getOSPrefersDarkColorScheme() { - // If the user uses a legacy browser that doesn't support matchMedia or - // doesn't support the prefers-color-scheme media query, this function returns - // false and AppShell will default to light mode - const userPrefersDark = - window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - return userPrefersDark; -} diff --git a/website/src/views/AppShell.tsx b/website/src/views/AppShell.tsx index 38430f4664..d8e74c5d39 100644 --- a/website/src/views/AppShell.tsx +++ b/website/src/views/AppShell.tsx @@ -1,7 +1,7 @@ import { FC, useCallback, useEffect, useState } from 'react'; import type { SemTimetableConfig } from 'types/timetables'; import type { Semester } from 'types/modules'; -import { DARK_MODE, DEFAULT_MODE } from 'types/settings'; +import { DARK_MODE } from 'types/settings'; import { Helmet } from 'react-helmet'; import { NavLink, useHistory } from 'react-router-dom'; @@ -115,7 +115,7 @@ const AppShell: FC = ({ children }) => { const isModuleListReady = moduleList.length; const mode = useSelector((state: State) => state.settings.mode); - const isDarkMode = mode === DARK_MODE || (mode === DEFAULT_MODE && getOSPrefersDarkColorScheme()); + const isDarkMode = mode === DARK_MODE; const theme = useSelector((state: State) => state.theme.id); From ca24f7731fa9b666bb563a1b795084ebf91ef50d Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 16:05:45 +0800 Subject: [PATCH 10/17] feat: add color scheme selection, remove toggle --- .../__snapshots__/settings.test.ts.snap | 10 +++--- website/src/actions/settings.test.ts | 10 ++---- website/src/actions/settings.ts | 16 +++------- website/src/entry/export/main.tsx | 7 +++-- website/src/reducers/settings.test.ts | 26 +++++++--------- website/src/reducers/settings.ts | 20 ++++++------ website/src/types/export.ts | 4 +-- website/src/types/reducers.ts | 4 +-- website/src/types/settings.ts | 25 ++++++++++++--- website/src/utils/css.ts | 4 +++ website/src/utils/export.ts | 2 +- website/src/views/AppShell.tsx | 8 ++--- .../views/components/KeyboardShortcuts.tsx | 31 ++++++++++++------- .../components/disqus/DisqusComments.tsx | 8 ++--- website/src/views/hooks/useColorScheme.tsx | 29 +++++++++++++++++ website/src/views/settings/ModeSelect.tsx | 29 ++++++++++------- .../src/views/settings/SettingsContainer.tsx | 16 +++++----- 17 files changed, 150 insertions(+), 99 deletions(-) create mode 100644 website/src/views/hooks/useColorScheme.tsx diff --git a/website/src/actions/__snapshots__/settings.test.ts.snap b/website/src/actions/__snapshots__/settings.test.ts.snap index d1be0c35bc..e77b7b354d 100644 --- a/website/src/actions/__snapshots__/settings.test.ts.snap +++ b/website/src/actions/__snapshots__/settings.test.ts.snap @@ -21,16 +21,16 @@ exports[`settings should dispatch a select of a semester value 1`] = ` } `; -exports[`settings should dispatch a selection of a mode 1`] = ` +exports[`settings should dispatch a selection of a color scheme preference 1`] = ` { - "payload": "LIGHT", - "type": "SELECT_MODE", + "payload": "LIGHT_COLOR_SCHEME_PREFERENCE", + "type": "SELECT_COLOR_SCHEME", } `; -exports[`settings should dispatch a toggle of a mode 1`] = ` +exports[`settings should dispatch a toggle of a color scheme preference 1`] = ` { "payload": null, - "type": "TOGGLE_MODE", + "type": "TOGGLE_COLOR_SCHEME", } `; diff --git a/website/src/actions/settings.test.ts b/website/src/actions/settings.test.ts index 6a5191a390..1e16dcc90a 100644 --- a/website/src/actions/settings.test.ts +++ b/website/src/actions/settings.test.ts @@ -2,7 +2,7 @@ import { Faculty, Semester } from 'types/modules'; import * as actions from 'actions/settings'; -import { LIGHT_MODE } from 'types/settings'; +import { LIGHT_COLOR_SCHEME_PREFERENCE } from 'types/settings'; describe('settings', () => { test('should dispatch a select of a semester value', () => { @@ -15,12 +15,8 @@ describe('settings', () => { expect(actions.selectNewStudent(newStudent)).toMatchSnapshot(); }); - test('should dispatch a selection of a mode', () => { - expect(actions.selectMode(LIGHT_MODE)).toMatchSnapshot(); - }); - - test('should dispatch a toggle of a mode', () => { - expect(actions.toggleMode()).toMatchSnapshot(); + test('should dispatch a selection of a color scheme preference', () => { + expect(actions.selectColorScheme(LIGHT_COLOR_SCHEME_PREFERENCE)).toMatchSnapshot(); }); test('should dispatch a select of a faculty value', () => { diff --git a/website/src/actions/settings.ts b/website/src/actions/settings.ts index a9ffab3315..271be2905b 100644 --- a/website/src/actions/settings.ts +++ b/website/src/actions/settings.ts @@ -1,5 +1,5 @@ import { Faculty, Semester } from 'types/modules'; -import { Mode } from 'types/settings'; +import { ColorSchemePreference } from 'types/settings'; import { ModuleTableOrder } from 'types/reducers'; import { RegPeriod, ScheduleType } from 'config'; @@ -29,22 +29,14 @@ export function selectFaculty(faculty: Faculty) { }; } -export const SELECT_MODE = 'SELECT_MODE' as const; -export function selectMode(mode: Mode) { +export const SELECT_COLOR_SCHEME = 'SELECT_COLOR_SCHEME' as const; +export function selectColorScheme(mode: ColorSchemePreference) { return { - type: SELECT_MODE, + type: SELECT_COLOR_SCHEME, payload: mode, }; } -export const TOGGLE_MODE = 'TOGGLE_MODE' as const; -export function toggleMode() { - return { - type: TOGGLE_MODE, - payload: null, - }; -} - export const DISMISS_MODREG_NOTIFICATION = 'DISMISS_MODREG_NOTIFICATION' as const; export function dismissModregNotification(round: RegPeriod) { return { diff --git a/website/src/entry/export/main.tsx b/website/src/entry/export/main.tsx index 50780d26ae..3ac008feaa 100644 --- a/website/src/entry/export/main.tsx +++ b/website/src/entry/export/main.tsx @@ -7,7 +7,7 @@ import { ExportData } from 'types/export'; import configureStore from 'bootstrapping/configure-store'; import { setExportedData } from 'actions/export'; -import { DARK_MODE } from 'types/settings'; +import { DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings'; import { State as StoreState } from 'types/state'; import TimetableOnly from './TimetableOnly'; @@ -32,7 +32,10 @@ window.setData = function setData(modules, data, callback) { const { semester, timetable, colors } = data; if (document.body) { - document.body.classList.toggle('mode-dark', data.settings.mode === DARK_MODE); + document.body.classList.toggle( + 'mode-dark', + data.settings.mode === DARK_COLOR_SCHEME_PREFERENCE, + ); } store.dispatch(setExportedData(modules, data)); diff --git a/website/src/reducers/settings.test.ts b/website/src/reducers/settings.test.ts index 1289e7ec23..621cebc883 100644 --- a/website/src/reducers/settings.test.ts +++ b/website/src/reducers/settings.test.ts @@ -3,14 +3,18 @@ import { SettingsState } from 'types/reducers'; import * as actions from 'actions/settings'; import reducer from 'reducers/settings'; -import { DARK_MODE, LIGHT_MODE } from 'types/settings'; +import { + DARK_COLOR_SCHEME_PREFERENCE, + LIGHT_COLOR_SCHEME_PREFERENCE, + SYSTEM_COLOR_SCHEME_PREFERENCE, +} from 'types/settings'; import { initAction, rehydrateAction } from 'test-utils/redux'; import config, { RegPeriod } from 'config'; const initialState: SettingsState = { newStudent: false, faculty: '', - mode: LIGHT_MODE, + colorScheme: SYSTEM_COLOR_SCHEME_PREFERENCE, hiddenInTimetable: [], modRegNotification: { enabled: true, @@ -25,7 +29,10 @@ const initialState: SettingsState = { const settingsWithNewStudent: SettingsState = { ...initialState, newStudent: true }; const faculty = 'School of Computing'; const settingsWithFaculty: SettingsState = { ...initialState, faculty }; -const settingsWithDarkMode: SettingsState = { ...initialState, mode: DARK_MODE }; +const settingsWithDarkMode: SettingsState = { + ...initialState, + colorScheme: DARK_COLOR_SCHEME_PREFERENCE, +}; const settingsWithDismissedNotifications: SettingsState = produce(initialState, (draft) => { draft.modRegNotification.dismissed = [ { type: 'Select Courses', name: '1' }, @@ -53,24 +60,15 @@ describe('settings', () => { }); test('can select mode', () => { - const action = actions.selectMode(DARK_MODE); + const action = actions.selectColorScheme(DARK_COLOR_SCHEME_PREFERENCE); const nextState: SettingsState = reducer(initialState, action); expect(nextState).toEqual(settingsWithDarkMode); - const action2 = actions.selectMode(LIGHT_MODE); + const action2 = actions.selectColorScheme(LIGHT_COLOR_SCHEME_PREFERENCE); const nextState2: SettingsState = reducer(nextState, action2); expect(nextState2).toEqual(initialState); }); - test('can toggle mode', () => { - const action = actions.toggleMode(); - const nextState: SettingsState = reducer(initialState, action); - expect(nextState).toEqual(settingsWithDarkMode); - - const nextState2: SettingsState = reducer(nextState, action); - expect(nextState2).toEqual(initialState); - }); - test('set module table order', () => { const state1 = reducer(initialState, actions.setModuleTableOrder('mc')); expect(state1.moduleTableOrder).toEqual('mc'); diff --git a/website/src/reducers/settings.ts b/website/src/reducers/settings.ts index c3c7ad7d4d..3c0bc969c2 100644 --- a/website/src/reducers/settings.ts +++ b/website/src/reducers/settings.ts @@ -9,18 +9,21 @@ import { DISMISS_MODREG_NOTIFICATION, ENABLE_MODREG_NOTIFICATION, SELECT_FACULTY, - SELECT_MODE, + SELECT_COLOR_SCHEME, SELECT_NEW_STUDENT, SET_LOAD_DISQUS_MANUALLY, SET_MODULE_TABLE_SORT, TOGGLE_BETA_TESTING_STATUS, TOGGLE_MODREG_NOTIFICATION_GLOBALLY, - TOGGLE_MODE, SET_MODREG_SCHEDULE_TYPE, } from 'actions/settings'; import { SET_EXPORTED_DATA } from 'actions/constants'; import { DIMENSIONS, withTracker } from 'bootstrapping/matomo'; -import { SYSTEM_MODE } from 'types/settings'; +import { + DARK_COLOR_SCHEME_PREFERENCE, + LIGHT_COLOR_SCHEME_PREFERENCE, + SYSTEM_COLOR_SCHEME_PREFERENCE, +} from 'types/settings'; import config from 'config'; import { isRoundDismissed } from 'selectors/modreg'; @@ -34,7 +37,7 @@ export const defaultModRegNotificationState = { const defaultSettingsState: SettingsState = { newStudent: false, faculty: '', - mode: SYSTEM_MODE, + colorScheme: SYSTEM_COLOR_SCHEME_PREFERENCE, hiddenInTimetable: [], modRegNotification: defaultModRegNotificationState, moduleTableOrder: 'exam', @@ -54,16 +57,11 @@ function settings(state: SettingsState = defaultSettingsState, action: Actions): ...state, faculty: action.payload, }; - case SELECT_MODE: + case SELECT_COLOR_SCHEME: return { ...state, - mode: action.payload, + colorScheme: action.payload, }; - case TOGGLE_MODE: - return { - ...state, - }; - case TOGGLE_MODREG_NOTIFICATION_GLOBALLY: return produce(state, (draft) => { draft.modRegNotification.enabled = action.payload.enabled; diff --git a/website/src/types/export.ts b/website/src/types/export.ts index 2710271e01..c1c0855873 100644 --- a/website/src/types/export.ts +++ b/website/src/types/export.ts @@ -1,6 +1,6 @@ import { SemTimetableConfig } from 'types/timetables'; import { Semester, ModuleCode } from 'types/modules'; -import { Mode } from 'types/settings'; +import { ColorSchemePreference } from 'types/settings'; import { ColorMapping, ThemeState } from 'types/reducers'; export type ExportData = { @@ -10,6 +10,6 @@ export type ExportData = { readonly hidden: ModuleCode[]; readonly theme: ThemeState; readonly settings: { - mode: Mode; + colorScheme: ColorSchemePreference; }; }; diff --git a/website/src/types/reducers.ts b/website/src/types/reducers.ts index cd37df9e8f..5418bb7d6f 100644 --- a/website/src/types/reducers.ts +++ b/website/src/types/reducers.ts @@ -1,7 +1,7 @@ import { AxiosError } from 'axios'; import { RegPeriodType, ScheduleType } from 'config'; -import { Mode } from './settings'; +import { ColorSchemePreference } from './settings'; import { ColorIndex, Lesson, TimetableConfig } from './timetables'; import { Faculty, @@ -98,7 +98,7 @@ export type ModuleTableOrder = 'exam' | 'mc' | 'code'; export type SettingsState = { readonly newStudent: boolean; readonly faculty: Faculty | null; - readonly mode: Mode; + readonly colorScheme: ColorSchemePreference; readonly hiddenInTimetable: ModuleCode[]; readonly modRegNotification: ModRegNotificationSettings; readonly moduleTableOrder: ModuleTableOrder; diff --git a/website/src/types/settings.ts b/website/src/types/settings.ts index 859e5a6fa2..c3a0679c10 100644 --- a/website/src/types/settings.ts +++ b/website/src/types/settings.ts @@ -1,12 +1,29 @@ export type ThemeId = string; +/** + * Themes are the "color palette" of the website. They define the set of colors + * being used across the website. + */ export type Theme = { readonly id: ThemeId; readonly name: string; }; -export type Mode = 'SYSTEM' | 'LIGHT' | 'DARK'; +/** + * Color schemes simply define whether the website is in light or dark mode. This + * is not to be confused with themese, which define the color palette of the website. + */ +export type ColorScheme = 'LIGHT_COLOR_SCHEME' | 'DARK_COLOR_SCHEME'; -export const SYSTEM_MODE: Mode = 'SYSTEM'; -export const LIGHT_MODE: Mode = 'LIGHT'; -export const DARK_MODE: Mode = 'DARK'; +export const LIGHT_COLOR_SCHEME: ColorScheme = 'LIGHT_COLOR_SCHEME'; +export const DARK_COLOR_SCHEME: ColorScheme = 'DARK_COLOR_SCHEME'; + +export type ColorSchemePreference = + | 'SYSTEM_COLOR_SCHEME_PREFERENCE' + | 'LIGHT_COLOR_SCHEME_PREFERENCE' + | 'DARK_COLOR_SCHEME_PREFERENCE'; + +export const SYSTEM_COLOR_SCHEME_PREFERENCE: ColorSchemePreference = + 'SYSTEM_COLOR_SCHEME_PREFERENCE'; +export const LIGHT_COLOR_SCHEME_PREFERENCE: ColorSchemePreference = 'LIGHT_COLOR_SCHEME_PREFERENCE'; +export const DARK_COLOR_SCHEME_PREFERENCE: ColorSchemePreference = 'DARK_COLOR_SCHEME_PREFERENCE'; diff --git a/website/src/utils/css.ts b/website/src/utils/css.ts index 209cb808e3..cc4ff72dc8 100644 --- a/website/src/utils/css.ts +++ b/website/src/utils/css.ts @@ -28,6 +28,10 @@ export function breakpointUp(size: Breakpoint) { return { minWidth: breakpoints[size] } satisfies QueryObject; } +export function prefersColorScheme(colorScheme: 'light' | 'dark') { + return { prefersColorScheme: colorScheme } satisfies QueryObject; +} + export function touchScreenOnly() { return { pointer: 'coarse' } satisfies QueryObject; } diff --git a/website/src/utils/export.ts b/website/src/utils/export.ts index 7b0e97d047..0b3775bf8d 100644 --- a/website/src/utils/export.ts +++ b/website/src/utils/export.ts @@ -19,7 +19,7 @@ export function extractStateForExport( hidden, theme: state.theme, settings: { - mode: state.settings.mode, + colorScheme: state.settings.colorScheme, }, }; } diff --git a/website/src/views/AppShell.tsx b/website/src/views/AppShell.tsx index d8e74c5d39..6cb57ade07 100644 --- a/website/src/views/AppShell.tsx +++ b/website/src/views/AppShell.tsx @@ -1,7 +1,7 @@ import { FC, useCallback, useEffect, useState } from 'react'; import type { SemTimetableConfig } from 'types/timetables'; import type { Semester } from 'types/modules'; -import { DARK_MODE } from 'types/settings'; +import { DARK_COLOR_SCHEME } from 'types/settings'; import { Helmet } from 'react-helmet'; import { NavLink, useHistory } from 'react-router-dom'; @@ -30,12 +30,12 @@ import Logo from 'img/nusmods-logo.svg'; import type { Dispatch } from 'types/redux'; import type { State } from 'types/state'; import type { Actions } from 'types/actions'; -import { getOSPrefersDarkColorScheme } from 'utils/css'; import LoadingSpinner from './components/LoadingSpinner'; import FeedbackModal from './components/FeedbackModal'; import KeyboardShortcuts from './components/KeyboardShortcuts'; import styles from './AppShell.scss'; +import useColorScheme from './hooks/useColorScheme'; /** * Fetch module list on mount. @@ -114,8 +114,8 @@ const AppShell: FC = ({ children }) => { const moduleList = useSelector((state: State) => state.moduleBank.moduleList); const isModuleListReady = moduleList.length; - const mode = useSelector((state: State) => state.settings.mode); - const isDarkMode = mode === DARK_MODE; + const colorScheme = useColorScheme(); + const isDarkMode = colorScheme === DARK_COLOR_SCHEME; const theme = useSelector((state: State) => state.theme.id); diff --git a/website/src/views/components/KeyboardShortcuts.tsx b/website/src/views/components/KeyboardShortcuts.tsx index abb9303b3f..b6e323497e 100644 --- a/website/src/views/components/KeyboardShortcuts.tsx +++ b/website/src/views/components/KeyboardShortcuts.tsx @@ -4,14 +4,19 @@ import { useDispatch, useStore } from 'react-redux'; import Mousetrap from 'mousetrap'; import { groupBy, map } from 'lodash'; -import { DARK_MODE } from 'types/settings'; +import { + DARK_COLOR_SCHEME, + DARK_COLOR_SCHEME_PREFERENCE, + LIGHT_COLOR_SCHEME_PREFERENCE, +} from 'types/settings'; import themes from 'data/themes.json'; import { cycleTheme, toggleTimetableOrientation } from 'actions/theme'; import { openNotification } from 'actions/app'; -import { toggleMode } from 'actions/settings'; +import { selectColorScheme } from 'actions/settings'; import { intersperse } from 'utils/array'; import ComponentMap from 'utils/ComponentMap'; import type { State } from 'types/state'; +import useColorScheme from 'views/hooks/useColorScheme'; import Modal from './Modal'; import styles from './KeyboardShortcuts.scss'; @@ -31,6 +36,7 @@ const THEME_NOTIFICATION_TIMEOUT = 1000; const KeyboardShortcuts: React.FC = () => { const [helpShown, setHelpShown] = useState(false); + const colorScheme = useColorScheme(); const closeModal = useCallback(() => setHelpShown(false), []); const store = useStore(); @@ -99,16 +105,19 @@ const KeyboardShortcuts: React.FC = () => { // Toggle night mode bind('x', APPEARANCE, 'Toggle Night Mode', () => { - dispatch(toggleMode()); - - // We fetch the current mode from the redux store directly, instead of - // using useSelector, as useSelector will capture the old stale value - const { mode } = store.getState().settings; + const newColorScheme = + colorScheme === DARK_COLOR_SCHEME + ? LIGHT_COLOR_SCHEME_PREFERENCE + : DARK_COLOR_SCHEME_PREFERENCE; + dispatch(selectColorScheme(newColorScheme)); dispatch( - openNotification(`Night mode ${mode === DARK_MODE ? 'on' : 'off'}`, { - overwritable: true, - }), + openNotification( + `Night mode ${newColorScheme === DARK_COLOR_SCHEME_PREFERENCE ? 'on' : 'off'}`, + { + overwritable: true, + }, + ), ); }); @@ -148,7 +157,7 @@ const KeyboardShortcuts: React.FC = () => { shortcuts.current.forEach(({ key }) => Mousetrap.unbind(key)); shortcuts.current = []; }; - }, [dispatch, helpShown, history, store]); + }, [dispatch, helpShown, colorScheme, history, store]); function renderShortcut(shortcut: Shortcut): React.ReactNode { if (typeof shortcut === 'string') { diff --git a/website/src/views/components/disqus/DisqusComments.tsx b/website/src/views/components/disqus/DisqusComments.tsx index 1a592a456c..d7bb9c0219 100644 --- a/website/src/views/components/disqus/DisqusComments.tsx +++ b/website/src/views/components/disqus/DisqusComments.tsx @@ -3,7 +3,7 @@ import classnames from 'classnames'; import { connect } from 'react-redux'; import { MessageSquare } from 'react-feather'; -import { Mode } from 'types/settings'; +import { ColorSchemePreference } from 'types/settings'; import config from 'config'; import { DisqusConfig } from 'types/views'; import insertScript from 'utils/insertScript'; @@ -15,7 +15,7 @@ type Props = DisqusConfig & { // Disqus autodetects page background color so that its own font color has // enough contrast to be read, but only when the widget is loaded, so we use // this to reload the widget after night mode is activated or deactivated - mode: Mode; + colorScheme: ColorSchemePreference; loadDisqusManually: boolean; }; @@ -39,7 +39,7 @@ class DisqusComments extends PureComponent { // Wait a bit for the page colors to change before reloading instance // 2 second delay is found empirically, and is longer than necessary to // account for lag in slower user agents - if (prevProps.mode !== this.props.mode) { + if (prevProps.colorScheme !== this.props.colorScheme) { setTimeout(this.loadInstance, 2000); } else { this.loadInstance(); @@ -105,5 +105,5 @@ class DisqusComments extends PureComponent { export default connect((state: StoreState) => ({ loadDisqusManually: state.settings.loadDisqusManually, - mode: state.settings.mode, + colorScheme: state.settings.colorScheme, }))(DisqusComments); diff --git a/website/src/views/hooks/useColorScheme.tsx b/website/src/views/hooks/useColorScheme.tsx new file mode 100644 index 0000000000..6e6b490068 --- /dev/null +++ b/website/src/views/hooks/useColorScheme.tsx @@ -0,0 +1,29 @@ +import { + DARK_COLOR_SCHEME, + DARK_COLOR_SCHEME_PREFERENCE, + LIGHT_COLOR_SCHEME, + LIGHT_COLOR_SCHEME_PREFERENCE, + SYSTEM_COLOR_SCHEME_PREFERENCE, +} from 'types/settings'; +import type { ColorScheme } from 'types/settings'; +import { prefersColorScheme } from 'utils/css'; +import { useSelector } from 'react-redux'; +import type { State } from 'types/state'; +import useMediaQuery from './useMediaQuery'; + +/** + * @returns Whether the user's (operating) system prefers dark mode. + */ +export default function useColorScheme(): ColorScheme { + const colorSchemePreference = useSelector((state: State) => state.settings.colorScheme); + const systemPrefersDarkColorScheme = useMediaQuery(prefersColorScheme('dark')); + switch (colorSchemePreference) { + case SYSTEM_COLOR_SCHEME_PREFERENCE: + return systemPrefersDarkColorScheme ? DARK_COLOR_SCHEME : LIGHT_COLOR_SCHEME; + case DARK_COLOR_SCHEME_PREFERENCE: + return DARK_COLOR_SCHEME; + case LIGHT_COLOR_SCHEME_PREFERENCE: + default: + return LIGHT_COLOR_SCHEME; + } +} diff --git a/website/src/views/settings/ModeSelect.tsx b/website/src/views/settings/ModeSelect.tsx index 22a23ad995..69f99558ee 100644 --- a/website/src/views/settings/ModeSelect.tsx +++ b/website/src/views/settings/ModeSelect.tsx @@ -1,40 +1,45 @@ import * as React from 'react'; import classnames from 'classnames'; -import { Mode, DEFAULT_MODE, LIGHT_MODE, DARK_MODE } from 'types/settings'; +import { + ColorSchemePreference, + SYSTEM_COLOR_SCHEME_PREFERENCE, + LIGHT_COLOR_SCHEME_PREFERENCE, + DARK_COLOR_SCHEME_PREFERENCE, +} from 'types/settings'; type Props = { - mode: Mode; - onSelectMode: (mode: Mode) => void; + colorScheme: ColorSchemePreference; + onSelectColorScheme: (mode: ColorSchemePreference) => void; }; -type ModeOption = { value: Mode; label: string }; +type ModeOption = { value: ColorSchemePreference; label: string }; const MODES: ModeOption[] = [ { - label: 'Auto-detect', - value: DEFAULT_MODE, + label: 'Auto', + value: SYSTEM_COLOR_SCHEME_PREFERENCE, }, { label: 'On', - value: DARK_MODE, + value: DARK_COLOR_SCHEME_PREFERENCE, }, { label: 'Off', - value: LIGHT_MODE, + value: LIGHT_COLOR_SCHEME_PREFERENCE, }, ]; -const ModeSelect: React.FC = ({ mode, onSelectMode }) => ( +const ModeSelect: React.FC = ({ colorScheme, onSelectColorScheme }) => (
{MODES.map(({ value, label }) => ( diff --git a/website/src/views/settings/SettingsContainer.tsx b/website/src/views/settings/SettingsContainer.tsx index 5912759748..125f73ecde 100644 --- a/website/src/views/settings/SettingsContainer.tsx +++ b/website/src/views/settings/SettingsContainer.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import classnames from 'classnames'; import { isEqual } from 'lodash'; -import { Mode, ThemeId } from 'types/settings'; +import { ColorSchemePreference, ThemeId } from 'types/settings'; import { Tracker } from 'types/vendor/piwik'; import { ModRegNotificationSettings } from 'types/reducers'; import { State as StoreState } from 'types/state'; @@ -15,7 +15,7 @@ import { dismissModregNotification, enableModRegNotification, selectFaculty, - selectMode, + selectColorScheme, setLoadDisqusManually, setModRegScheduleType, toggleBetaTesting, @@ -42,13 +42,13 @@ import styles from './SettingsContainer.scss'; type Props = { currentThemeId: string; - mode: Mode; + colorScheme: ColorSchemePreference; betaTester: boolean; loadDisqusManually: boolean; modRegNotification: ModRegNotificationSettings; selectTheme: (theme: ThemeId) => void; - selectMode: (mode: Mode) => void; + selectColorScheme: (colorScheme: ColorSchemePreference) => void; toggleBetaTesting: () => void; setLoadDisqusManually: (status: boolean) => void; @@ -61,7 +61,7 @@ type Props = { const SettingsContainer: React.FC = ({ currentThemeId, - mode, + colorScheme, betaTester, loadDisqusManually, modRegNotification, @@ -114,7 +114,7 @@ const SettingsContainer: React.FC = ({

- +

@@ -294,7 +294,7 @@ const SettingsContainer: React.FC = ({ }; const mapStateToProps = (state: StoreState) => ({ - mode: state.settings.mode, + colorScheme: state.settings.colorScheme, currentThemeId: state.theme.id, betaTester: state.settings.beta || false, loadDisqusManually: state.settings.loadDisqusManually, @@ -304,7 +304,7 @@ const mapStateToProps = (state: StoreState) => ({ const connectedSettings = connect(mapStateToProps, { selectTheme, selectFaculty, - selectMode, + selectColorScheme, toggleBetaTesting, setLoadDisqusManually, toggleModRegNotificationGlobally, From fc25b6642742d7980db2f86f9e04f9ed652103b7 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 16:25:36 +0800 Subject: [PATCH 11/17] refactor: ExportMenu into functional component --- website/src/entry/export/main.tsx | 7 +- website/src/views/timetable/ExportMenu.tsx | 229 ++++++++++----------- 2 files changed, 111 insertions(+), 125 deletions(-) diff --git a/website/src/entry/export/main.tsx b/website/src/entry/export/main.tsx index 3ac008feaa..ea9d6523a6 100644 --- a/website/src/entry/export/main.tsx +++ b/website/src/entry/export/main.tsx @@ -7,7 +7,7 @@ import { ExportData } from 'types/export'; import configureStore from 'bootstrapping/configure-store'; import { setExportedData } from 'actions/export'; -import { DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings'; +import { DARK_COLOR_SCHEME, DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings'; import { State as StoreState } from 'types/state'; import TimetableOnly from './TimetableOnly'; @@ -32,10 +32,7 @@ window.setData = function setData(modules, data, callback) { const { semester, timetable, colors } = data; if (document.body) { - document.body.classList.toggle( - 'mode-dark', - data.settings.mode === DARK_COLOR_SCHEME_PREFERENCE, - ); + document.body.classList.toggle('mode-dark', data.settings.colorScheme === DARK_COLOR_SCHEME); } store.dispatch(setExportedData(modules, data)); diff --git a/website/src/views/timetable/ExportMenu.tsx b/website/src/views/timetable/ExportMenu.tsx index 2797e2dd0c..87b0980347 100644 --- a/website/src/views/timetable/ExportMenu.tsx +++ b/website/src/views/timetable/ExportMenu.tsx @@ -1,6 +1,6 @@ -import { PureComponent } from 'react'; +import { useCallback, useState } from 'react'; import Downshift, { ChildrenFunction } from 'downshift'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; import { AlertTriangle, Calendar, ChevronDown, Download, FileText, Image } from 'react-feather'; @@ -14,7 +14,7 @@ import Online from 'views/components/Online'; import Modal from 'views/components/Modal'; import ComponentMap from 'utils/ComponentMap'; import { Counter } from 'utils/react'; -import { State as StoreState } from 'types/state'; +import { State } from 'types/state'; import styles from './ExportMenu.scss'; @@ -24,129 +24,118 @@ const IMAGE: ExportAction = 'IMAGE'; const PDF: ExportAction = 'PDF'; type Props = { - state: StoreState; semester: Semester; timetable: SemTimetableConfig; - downloadAsIcal: (semester: Semester) => void; }; -type State = { - isMacWarningOpen: boolean; -}; +const ExportMenuComponent: React.FC = ({ semester, timetable }) => { + const [isMacWarningOpen, setIsMacWarningOpen] = useState(false); -export class ExportMenuComponent extends PureComponent { - override state: State = { - isMacWarningOpen: false, - }; - - onSelect = (item: ExportAction | null) => { - if (item === CALENDAR) { - this.props.downloadAsIcal(this.props.semester); - - // macOS calendar client has a ridiculous bug where it would sometimes disregard - // EXDATE statements which causes events to show up during holidays, recess week, etc. - if (navigator.platform === 'MacIntel') { - this.setState({ - isMacWarningOpen: true, - }); - } - } - }; - - closeMacOSWarningModal = () => this.setState({ isMacWarningOpen: false }); - - renderDropdown: ChildrenFunction = ({ - isOpen, - getItemProps, - getMenuProps, - toggleMenu, - highlightedIndex, - }) => { - const { semester, timetable, state } = this.props; - const counter = new Counter(); - - return ( -
- - -
- - - Image (.png) - - - - PDF (.pdf) - - - - {SUPPORTS_DOWNLOAD && ( - - )} -
+ const dispatch = useDispatch(); + const state = useSelector((storeState: State) => storeState); - -
- -

The calendar you have just downloaded may not work with the macOS Calendar app.

-
-
- - Find out more - - + const onSelect = useCallback( + (item: ExportAction | null) => { + if (item === CALENDAR) { + dispatch(downloadAsIcal(semester)); + + // macOS calendar client has a ridiculous bug where it would sometimes disregard + // EXDATE statements which causes events to show up during holidays, recess week, etc. + if (navigator.platform === 'MacIntel') { + setIsMacWarningOpen(true); + } + } + }, + [semester, dispatch], + ); + + const closeMacOSWarningModal = useCallback( + () => setIsMacWarningOpen(false), + [setIsMacWarningOpen], + ); + + const renderDropdown: ChildrenFunction = useCallback( + ({ isOpen, getItemProps, getMenuProps, toggleMenu, highlightedIndex }) => { + const counter = new Counter(); + + return ( +
+ + +
+ + + Image (.png) + + + + PDF (.pdf) + + + + {SUPPORTS_DOWNLOAD && ( + + )}
- -
- ); - }; - override render() { - return {this.renderDropdown}; - } -} + +
+ +

The calendar you have just downloaded may not work with the macOS Calendar app.

+
+
+ + Find out more + + +
+
+
+ ); + }, + [isMacWarningOpen, closeMacOSWarningModal, semester, timetable, state], + ); + + return {renderDropdown}; +}; -export default connect((state: StoreState) => ({ state }), { downloadAsIcal })(ExportMenuComponent); +export default ExportMenuComponent; From d824b9b4e26d72b02eb5a860dab5a7c9a64313c3 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 16:28:26 +0800 Subject: [PATCH 12/17] fix: add colorscheme to export preferences --- website/src/apis/export.ts | 22 +++++++++++++++++----- website/src/types/export.ts | 4 ++-- website/src/utils/export.ts | 4 +++- website/src/views/timetable/ExportMenu.tsx | 15 ++++++++++++--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/website/src/apis/export.ts b/website/src/apis/export.ts index 9abadede65..128fde0abd 100644 --- a/website/src/apis/export.ts +++ b/website/src/apis/export.ts @@ -4,6 +4,7 @@ import { Semester } from 'types/modules'; import { extractStateForExport } from 'utils/export'; import { State } from 'types/state'; import { SemTimetableConfig } from 'types/timetables'; +import { ColorScheme } from 'types/settings'; export type ExportOptions = { pixelRatio?: number; @@ -14,18 +15,29 @@ const baseUrl = 'https://export.nusmods.com/api/export'; function serializeState( semester: Semester, timetable: SemTimetableConfig, + colorScheme: ColorScheme, state: State, options: ExportOptions = {}, ) { return qs.stringify({ - data: JSON.stringify(extractStateForExport(semester, timetable, state)), + data: JSON.stringify(extractStateForExport(semester, timetable, colorScheme, state)), ...options, }); } export default { - image: (semester: Semester, timetable: SemTimetableConfig, state: State, pixelRatio = 1) => - `${baseUrl}/image?${serializeState(semester, timetable, state, { pixelRatio })}`, - pdf: (semester: Semester, timetable: SemTimetableConfig, state: State) => - `${baseUrl}/pdf?${serializeState(semester, timetable, state)}`, + image: ( + semester: Semester, + timetable: SemTimetableConfig, + colorScheme: ColorScheme, + state: State, + pixelRatio = 1, + ) => + `${baseUrl}/image?${serializeState(semester, timetable, colorScheme, state, { pixelRatio })}`, + pdf: ( + semester: Semester, + timetable: SemTimetableConfig, + colorScheme: ColorScheme, + state: State, + ) => `${baseUrl}/pdf?${serializeState(semester, timetable, colorScheme, state)}`, }; diff --git a/website/src/types/export.ts b/website/src/types/export.ts index c1c0855873..ad16e02eed 100644 --- a/website/src/types/export.ts +++ b/website/src/types/export.ts @@ -1,6 +1,6 @@ import { SemTimetableConfig } from 'types/timetables'; import { Semester, ModuleCode } from 'types/modules'; -import { ColorSchemePreference } from 'types/settings'; +import type { ColorScheme } from 'types/settings'; import { ColorMapping, ThemeState } from 'types/reducers'; export type ExportData = { @@ -10,6 +10,6 @@ export type ExportData = { readonly hidden: ModuleCode[]; readonly theme: ThemeState; readonly settings: { - colorScheme: ColorSchemePreference; + colorScheme: ColorScheme; }; }; diff --git a/website/src/utils/export.ts b/website/src/utils/export.ts index 0b3775bf8d..1d65ec3feb 100644 --- a/website/src/utils/export.ts +++ b/website/src/utils/export.ts @@ -3,10 +3,12 @@ import { ExportData } from 'types/export'; import { getSemesterTimetableColors } from 'selectors/timetables'; import { State } from 'types/state'; import { SemTimetableConfig } from 'types/timetables'; +import { ColorScheme } from 'types/settings'; export function extractStateForExport( semester: Semester, timetable: SemTimetableConfig, + colorScheme: ColorScheme, state: State, ): ExportData { const colors = getSemesterTimetableColors(state)(semester); @@ -19,7 +21,7 @@ export function extractStateForExport( hidden, theme: state.theme, settings: { - colorScheme: state.settings.colorScheme, + colorScheme, }, }; } diff --git a/website/src/views/timetable/ExportMenu.tsx b/website/src/views/timetable/ExportMenu.tsx index 87b0980347..91bc63e23d 100644 --- a/website/src/views/timetable/ExportMenu.tsx +++ b/website/src/views/timetable/ExportMenu.tsx @@ -16,6 +16,7 @@ import ComponentMap from 'utils/ComponentMap'; import { Counter } from 'utils/react'; import { State } from 'types/state'; +import useColorScheme from 'views/hooks/useColorScheme'; import styles from './ExportMenu.scss'; type ExportAction = 'CALENDAR' | 'IMAGE' | 'PDF'; @@ -34,6 +35,8 @@ const ExportMenuComponent: React.FC = ({ semester, timetable }) => { const dispatch = useDispatch(); const state = useSelector((storeState: State) => storeState); + const colorScheme = useColorScheme(); + const onSelect = useCallback( (item: ExportAction | null) => { if (item === CALENDAR) { @@ -79,7 +82,13 @@ const ExportMenuComponent: React.FC = ({ semester, timetable }) => { > = ({ semester, timetable }) => { = ({ semester, timetable }) => {
); }, - [isMacWarningOpen, closeMacOSWarningModal, semester, timetable, state], + [isMacWarningOpen, closeMacOSWarningModal, semester, timetable, colorScheme, state], ); return {renderDropdown}; From 005acad7ba5f1b4e5aec0179fd2d7bdd1c7720cf Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 16:37:07 +0800 Subject: [PATCH 13/17] fix: tests --- website/src/actions/settings.test.ts | 6 +++--- website/src/reducers/settings.test.ts | 14 +++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/website/src/actions/settings.test.ts b/website/src/actions/settings.test.ts index 1e16dcc90a..2e5d5ed999 100644 --- a/website/src/actions/settings.test.ts +++ b/website/src/actions/settings.test.ts @@ -1,9 +1,8 @@ import { Faculty, Semester } from 'types/modules'; +import { LIGHT_COLOR_SCHEME_PREFERENCE } from 'types/settings'; import * as actions from 'actions/settings'; -import { LIGHT_COLOR_SCHEME_PREFERENCE } from 'types/settings'; - describe('settings', () => { test('should dispatch a select of a semester value', () => { const semester: Semester = 1; @@ -16,7 +15,8 @@ describe('settings', () => { }); test('should dispatch a selection of a color scheme preference', () => { - expect(actions.selectColorScheme(LIGHT_COLOR_SCHEME_PREFERENCE)).toMatchSnapshot(); + const colorSchemePreference = LIGHT_COLOR_SCHEME_PREFERENCE; + expect(actions.selectColorScheme(colorSchemePreference)).toMatchSnapshot(); }); test('should dispatch a select of a faculty value', () => { diff --git a/website/src/reducers/settings.test.ts b/website/src/reducers/settings.test.ts index 621cebc883..9bca8cee3d 100644 --- a/website/src/reducers/settings.test.ts +++ b/website/src/reducers/settings.test.ts @@ -29,6 +29,10 @@ const initialState: SettingsState = { const settingsWithNewStudent: SettingsState = { ...initialState, newStudent: true }; const faculty = 'School of Computing'; const settingsWithFaculty: SettingsState = { ...initialState, faculty }; +const settingsWithLightMode: SettingsState = { + ...initialState, + colorScheme: LIGHT_COLOR_SCHEME_PREFERENCE, +}; const settingsWithDarkMode: SettingsState = { ...initialState, colorScheme: DARK_COLOR_SCHEME_PREFERENCE, @@ -59,14 +63,18 @@ describe('settings', () => { expect(nextState).toEqual(settingsWithFaculty); }); - test('can select mode', () => { + test('can select color scheme', () => { const action = actions.selectColorScheme(DARK_COLOR_SCHEME_PREFERENCE); const nextState: SettingsState = reducer(initialState, action); expect(nextState).toEqual(settingsWithDarkMode); const action2 = actions.selectColorScheme(LIGHT_COLOR_SCHEME_PREFERENCE); - const nextState2: SettingsState = reducer(nextState, action2); - expect(nextState2).toEqual(initialState); + const nextState2: SettingsState = reducer(initialState, action2); + expect(nextState2).toEqual(settingsWithLightMode); + + const action3 = actions.selectColorScheme(SYSTEM_COLOR_SCHEME_PREFERENCE); + const nextState3: SettingsState = reducer(nextState, action3); + expect(nextState3).toEqual(initialState); }); test('set module table order', () => { From f07172fc593ac1cf05163fcfc897286e82d24270 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 16:53:36 +0800 Subject: [PATCH 14/17] fix: type error in reducer --- website/src/reducers/settings.ts | 14 +++++++------- website/src/utils/colorScheme.ts | 19 +++++++++++++++++++ .../views/components/KeyboardShortcuts.tsx | 13 +++---------- 3 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 website/src/utils/colorScheme.ts diff --git a/website/src/reducers/settings.ts b/website/src/reducers/settings.ts index 3c0bc969c2..41dabe1cc5 100644 --- a/website/src/reducers/settings.ts +++ b/website/src/reducers/settings.ts @@ -19,13 +19,10 @@ import { } from 'actions/settings'; import { SET_EXPORTED_DATA } from 'actions/constants'; import { DIMENSIONS, withTracker } from 'bootstrapping/matomo'; -import { - DARK_COLOR_SCHEME_PREFERENCE, - LIGHT_COLOR_SCHEME_PREFERENCE, - SYSTEM_COLOR_SCHEME_PREFERENCE, -} from 'types/settings'; +import { SYSTEM_COLOR_SCHEME_PREFERENCE } from 'types/settings'; import config from 'config'; import { isRoundDismissed } from 'selectors/modreg'; +import { colorSchemeToPreference } from 'utils/colorScheme'; export const defaultModRegNotificationState = { semesterKey: config.getSemesterKey(), @@ -86,11 +83,14 @@ function settings(state: SettingsState = defaultSettingsState, action: Actions): draft.modRegNotification.scheduleType = action.payload; }); - case SET_EXPORTED_DATA: + case SET_EXPORTED_DATA: { + const { colorScheme, ...otherSettings } = action.payload.settings; return { ...state, - ...action.payload.settings, + ...otherSettings, + colorScheme: colorSchemeToPreference(action.payload.settings.colorScheme), }; + } case SET_MODULE_TABLE_SORT: return { diff --git a/website/src/utils/colorScheme.ts b/website/src/utils/colorScheme.ts new file mode 100644 index 0000000000..cb2d6ef6c9 --- /dev/null +++ b/website/src/utils/colorScheme.ts @@ -0,0 +1,19 @@ +import { + ColorScheme, + ColorSchemePreference, + DARK_COLOR_SCHEME, + DARK_COLOR_SCHEME_PREFERENCE, + LIGHT_COLOR_SCHEME, + LIGHT_COLOR_SCHEME_PREFERENCE, + SYSTEM_COLOR_SCHEME_PREFERENCE, +} from 'types/settings'; + +export function colorSchemeToPreference(colorScheme: ColorScheme) { + switch (colorScheme) { + case LIGHT_COLOR_SCHEME: + return LIGHT_COLOR_SCHEME_PREFERENCE; + case DARK_COLOR_SCHEME: + default: + return DARK_COLOR_SCHEME_PREFERENCE; + } +} diff --git a/website/src/views/components/KeyboardShortcuts.tsx b/website/src/views/components/KeyboardShortcuts.tsx index b6e323497e..d93d52d4cb 100644 --- a/website/src/views/components/KeyboardShortcuts.tsx +++ b/website/src/views/components/KeyboardShortcuts.tsx @@ -4,11 +4,7 @@ import { useDispatch, useStore } from 'react-redux'; import Mousetrap from 'mousetrap'; import { groupBy, map } from 'lodash'; -import { - DARK_COLOR_SCHEME, - DARK_COLOR_SCHEME_PREFERENCE, - LIGHT_COLOR_SCHEME_PREFERENCE, -} from 'types/settings'; +import { DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings'; import themes from 'data/themes.json'; import { cycleTheme, toggleTimetableOrientation } from 'actions/theme'; import { openNotification } from 'actions/app'; @@ -17,6 +13,7 @@ import { intersperse } from 'utils/array'; import ComponentMap from 'utils/ComponentMap'; import type { State } from 'types/state'; import useColorScheme from 'views/hooks/useColorScheme'; +import { colorSchemeToPreference } from 'utils/colorScheme'; import Modal from './Modal'; import styles from './KeyboardShortcuts.scss'; @@ -105,12 +102,8 @@ const KeyboardShortcuts: React.FC = () => { // Toggle night mode bind('x', APPEARANCE, 'Toggle Night Mode', () => { - const newColorScheme = - colorScheme === DARK_COLOR_SCHEME - ? LIGHT_COLOR_SCHEME_PREFERENCE - : DARK_COLOR_SCHEME_PREFERENCE; + const newColorScheme = colorSchemeToPreference(colorScheme); dispatch(selectColorScheme(newColorScheme)); - dispatch( openNotification( `Night mode ${newColorScheme === DARK_COLOR_SCHEME_PREFERENCE ? 'on' : 'off'}`, From 2f300c8f0430747c9ff389f404092c2924187580 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 17:04:21 +0800 Subject: [PATCH 15/17] chore: fix typecheck --- website/src/entry/export/main.tsx | 2 +- website/src/reducers/index.test.ts | 5 +++-- website/src/utils/colorScheme.ts | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/website/src/entry/export/main.tsx b/website/src/entry/export/main.tsx index ea9d6523a6..02c43d558a 100644 --- a/website/src/entry/export/main.tsx +++ b/website/src/entry/export/main.tsx @@ -7,7 +7,7 @@ import { ExportData } from 'types/export'; import configureStore from 'bootstrapping/configure-store'; import { setExportedData } from 'actions/export'; -import { DARK_COLOR_SCHEME, DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings'; +import { DARK_COLOR_SCHEME } from 'types/settings'; import { State as StoreState } from 'types/state'; import TimetableOnly from './TimetableOnly'; diff --git a/website/src/reducers/index.test.ts b/website/src/reducers/index.test.ts index 8a197c3845..bf7309a83f 100644 --- a/website/src/reducers/index.test.ts +++ b/website/src/reducers/index.test.ts @@ -3,6 +3,7 @@ import { VERTICAL } from 'types/reducers'; import reducers from 'reducers'; import { setExportedData } from 'actions/export'; import modules from '__mocks__/modules/index'; +import { DARK_COLOR_SCHEME, DARK_COLOR_SCHEME_PREFERENCE } from 'types/settings'; /* eslint-disable no-useless-computed-key */ @@ -34,7 +35,7 @@ const exportData: ExportData = { showTitle: true, }, settings: { - mode: 'DARK', + colorScheme: DARK_COLOR_SCHEME, }, }; @@ -78,7 +79,7 @@ test('reducers should set export data state', () => { }); expect(state.settings).toMatchObject({ - mode: 'DARK', + colorScheme: DARK_COLOR_SCHEME_PREFERENCE, }); expect(state.theme).toEqual({ diff --git a/website/src/utils/colorScheme.ts b/website/src/utils/colorScheme.ts index cb2d6ef6c9..21ddeef5f1 100644 --- a/website/src/utils/colorScheme.ts +++ b/website/src/utils/colorScheme.ts @@ -1,11 +1,9 @@ import { ColorScheme, - ColorSchemePreference, DARK_COLOR_SCHEME, DARK_COLOR_SCHEME_PREFERENCE, LIGHT_COLOR_SCHEME, LIGHT_COLOR_SCHEME_PREFERENCE, - SYSTEM_COLOR_SCHEME_PREFERENCE, } from 'types/settings'; export function colorSchemeToPreference(colorScheme: ColorScheme) { From 07d38e3a107affa466b37ae24fa0e4be6df0e128 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 17:39:31 +0800 Subject: [PATCH 16/17] chore: remove obsolete snapshot --- website/src/actions/__snapshots__/settings.test.ts.snap | 7 ------- 1 file changed, 7 deletions(-) diff --git a/website/src/actions/__snapshots__/settings.test.ts.snap b/website/src/actions/__snapshots__/settings.test.ts.snap index e77b7b354d..d4c8c7b063 100644 --- a/website/src/actions/__snapshots__/settings.test.ts.snap +++ b/website/src/actions/__snapshots__/settings.test.ts.snap @@ -27,10 +27,3 @@ exports[`settings should dispatch a selection of a color scheme preference 1`] = "type": "SELECT_COLOR_SCHEME", } `; - -exports[`settings should dispatch a toggle of a color scheme preference 1`] = ` -{ - "payload": null, - "type": "TOGGLE_COLOR_SCHEME", -} -`; From d8f172c5e3ee6f1f397b5cc0c2186288c339e595 Mon Sep 17 00:00:00 2001 From: Ravern Koh Date: Wed, 3 Apr 2024 18:21:12 +0800 Subject: [PATCH 17/17] fix: toggling --- website/src/utils/colorScheme.ts | 10 ++++++++++ website/src/views/components/KeyboardShortcuts.tsx | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/website/src/utils/colorScheme.ts b/website/src/utils/colorScheme.ts index 21ddeef5f1..469d5b6611 100644 --- a/website/src/utils/colorScheme.ts +++ b/website/src/utils/colorScheme.ts @@ -15,3 +15,13 @@ export function colorSchemeToPreference(colorScheme: ColorScheme) { return DARK_COLOR_SCHEME_PREFERENCE; } } + +export function invertColorScheme(colorScheme: ColorScheme) { + switch (colorScheme) { + case LIGHT_COLOR_SCHEME: + return DARK_COLOR_SCHEME; + case DARK_COLOR_SCHEME: + default: + return LIGHT_COLOR_SCHEME; + } +} diff --git a/website/src/views/components/KeyboardShortcuts.tsx b/website/src/views/components/KeyboardShortcuts.tsx index d93d52d4cb..3ce7ae91f6 100644 --- a/website/src/views/components/KeyboardShortcuts.tsx +++ b/website/src/views/components/KeyboardShortcuts.tsx @@ -13,7 +13,7 @@ import { intersperse } from 'utils/array'; import ComponentMap from 'utils/ComponentMap'; import type { State } from 'types/state'; import useColorScheme from 'views/hooks/useColorScheme'; -import { colorSchemeToPreference } from 'utils/colorScheme'; +import { colorSchemeToPreference, invertColorScheme } from 'utils/colorScheme'; import Modal from './Modal'; import styles from './KeyboardShortcuts.scss'; @@ -102,7 +102,7 @@ const KeyboardShortcuts: React.FC = () => { // Toggle night mode bind('x', APPEARANCE, 'Toggle Night Mode', () => { - const newColorScheme = colorSchemeToPreference(colorScheme); + const newColorScheme = colorSchemeToPreference(invertColorScheme(colorScheme)); dispatch(selectColorScheme(newColorScheme)); dispatch( openNotification(