Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enable flexible number of colors for themes #3482

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f77edcf
feat: Enable flexible number of colors for themes
joeng03 Aug 3, 2023
b0392c7
fix: venue timetable renders white color lessons
joeng03 Aug 4, 2023
c8f0b38
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
joeng03 Aug 4, 2023
8563458
fix: linting errors
joeng03 Aug 4, 2023
0888fe2
Merge branch 'joeng03/support-flexible-num-of-colors-in-theme' of htt…
joeng03 Aug 4, 2023
bf7e006
refactor: refactor test cases to reflect current code modifications
joeng03 Aug 6, 2023
aeafe7c
feat: add action reassignAllModulesColor to reassign the color of the…
joeng03 Aug 7, 2023
1885a5c
Merge branch 'master' of https://github.com/joeng03/nusmods into joen…
joeng03 Aug 7, 2023
ef26201
fix(SettingsContainer): eslint.no-shadow
joeng03 Aug 7, 2023
2c34989
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
joeng03 Aug 7, 2023
6ec7da5
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
joeng03 Aug 9, 2023
52b4a53
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
joeng03 Aug 10, 2023
b97aa26
fix: lint error
joeng03 Aug 10, 2023
7ec797f
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
zwliew Aug 10, 2023
cbb938b
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
zwliew Aug 11, 2023
a4304f4
refactor: address changes requested
joeng03 Aug 13, 2023
927e7c9
merge changes from master
joeng03 Aug 13, 2023
a7ac61e
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
joeng03 Aug 14, 2023
0699b0a
Merge branch 'master' into joeng03/support-flexible-num-of-colors-in-…
zwliew Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion website/src/actions/__snapshots__/theme.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ exports[`theme should dispatch a cycle of theme 2`] = `

exports[`theme should dispatch a select of theme 1`] = `
{
"payload": "test",
"payload": {
"id": "test",
"name": "Test",
"numOfColors": 8,
},
"type": "SELECT_THEME",
}
`;
Expand Down
7 changes: 6 additions & 1 deletion website/src/actions/theme.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as actions from 'actions/theme';
import { Theme } from 'types/settings';

describe('theme', () => {
test('should dispatch a select of theme', () => {
const theme = 'test';
const theme: Theme = {
id: 'test',
name: 'Test',
numOfColors: 8,
};
expect(actions.selectTheme(theme)).toMatchSnapshot();
});

Expand Down
4 changes: 3 additions & 1 deletion website/src/actions/theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Theme } from 'types/settings';

export const SELECT_THEME = 'SELECT_THEME' as const;
export function selectTheme(theme: string) {
export function selectTheme(theme: Theme) {
return {
type: SELECT_THEME,
payload: theme,
Expand Down
21 changes: 19 additions & 2 deletions website/src/actions/timetables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ export const Internal = {
};
},

addModule(semester: Semester, moduleCode: ModuleCode, moduleLessonConfig: ModuleLessonConfig) {
addModule(
semester: Semester,
moduleCode: ModuleCode,
moduleLessonConfig: ModuleLessonConfig,
numOfColors: number,
) {
return {
type: ADD_MODULE,
payload: {
semester,
moduleCode,
moduleLessonConfig,
numOfColors,
},
};
},
Expand Down Expand Up @@ -64,8 +70,9 @@ export function addModule(semester: Semester, moduleCode: ModuleCode) {

const lessons = getModuleTimetable(module, semester);
const moduleLessonConfig = randomModuleLessonConfig(lessons);
const { numOfColors } = getState().theme;

dispatch(Internal.addModule(semester, moduleCode, moduleLessonConfig));
dispatch(Internal.addModule(semester, moduleCode, moduleLessonConfig, numOfColors));
});
}

Expand Down Expand Up @@ -207,6 +214,16 @@ export function selectModuleColor(
};
}

export const REASSIGN_ALL_MODULES_COLOR = 'REASSIGN_ALL_MODULES_COLOR' as const;
export function reassignAllModulesColor(numOfColors: number) {
return {
type: REASSIGN_ALL_MODULES_COLOR,
payload: {
numOfColors,
},
};
}

export const HIDE_LESSON_IN_TIMETABLE = 'HIDE_LESSON_IN_TIMETABLE' as const;
export function hideLessonInTimetable(semester: Semester, moduleCode: ModuleCode) {
return {
Expand Down
41 changes: 29 additions & 12 deletions website/src/data/themes.json
Original file line number Diff line number Diff line change
@@ -1,50 +1,67 @@
[
{
"id": "ashes",
"name": " Ashes"
"name": " Ashes",
"numOfColors": 8
},
{
"id": "chalk",
"name": "Chalk"
"name": "Chalk",
"numOfColors": 8
},
{
"id": "eighties",
"name": "Eighties"
"name": "Eighties",
"numOfColors": 8
},
{
"id": "google",
"name": "Google"
"name": "Google",
"numOfColors": 8
},
{
"id": "mocha",
"name": "Mocha"
"name": "Mocha",
"numOfColors": 8
},
{
"id": "monokai",
"name": "Monokai"
"name": "Monokai",
"numOfColors": 8
},
{
"id": "ocean",
"name": "Ocean"
"name": "Ocean",
"numOfColors": 8
},
{
"id": "oceanic-next",
"name": "Oceanic Next"
"name": "Oceanic Next",
"numOfColors": 8
},
{
"id": "paraiso",
"name": "Paraiso"
"name": "Paraiso",
"numOfColors": 8
},
{
"id": "railscasts",
"name": "Railscasts"
"name": "Railscasts",
"numOfColors": 8
},
{
"id": "tequila",
"name": "Tequila",
"numOfColors": 10
},
{
"id": "tomorrow",
"name": "Tomorrow"
"name": "Tomorrow",
"numOfColors": 8
},
{
"id": "twilight",
"name": "Twilight"
"name": "Twilight",
"numOfColors": 8
}
]
6 changes: 3 additions & 3 deletions website/src/entry/export/TimetableOnly.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ export default class TimetableOnly extends Component<Props, State> {

override render() {
const { store } = this.props;
const theme = store.getState().theme.id;
const { theme } = store.getState();

const { semester, timetable, colors } = this.state;
const timetableColors = fillColorMapping(timetable, colors);
const timetableColors = fillColorMapping(timetable, colors, theme.numOfColors);

return (
<MemoryRouter initialEntries={['https://nusmods.com']}>
<Provider store={store}>
<div id="timetable-only" className={`theme-${theme}`}>
<div id="timetable-only" className={`theme-${theme.id}`}>
<TimetableContent
header={null}
semester={semester}
Expand Down
2 changes: 2 additions & 0 deletions website/src/reducers/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const exportData: ExportData = {
hidden: ['PC1222'],
theme: {
id: 'google',
numOfColors: 8,
timetableOrientation: VERTICAL,
showTitle: true,
},
Expand Down Expand Up @@ -83,6 +84,7 @@ test('reducers should set export data state', () => {

expect(state.theme).toEqual({
id: 'google',
numOfColors: 8,
timetableOrientation: VERTICAL,
showTitle: true,
});
Expand Down
9 changes: 7 additions & 2 deletions website/src/reducers/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ import { ThemeState, VERTICAL } from 'types/reducers';

import * as actions from 'actions/theme';
import reducer, { defaultThemeState, themeIds } from 'reducers/theme';
import { Theme } from 'types/settings';

const themeInitialState: ThemeState = defaultThemeState;
const googleTheme = 'google';
const themeWithEighties: ThemeState = { ...themeInitialState, id: googleTheme };
const googleTheme: Theme = {
id: 'google',
name: 'Google',
numOfColors: 8,
};
const themeWithEighties: ThemeState = { ...themeInitialState, id: googleTheme.id };
const themeWithFirstTheme: ThemeState = { ...themeInitialState, id: themeIds[0] };
const themeWithLastTheme: ThemeState = { ...themeInitialState, id: themeIds[themeIds.length - 1] };
const themeWithVerticalOrientation: ThemeState = {
Expand Down
11 changes: 7 additions & 4 deletions website/src/reducers/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,34 @@ import { DIMENSIONS, withTracker } from 'bootstrapping/matomo';
export const defaultThemeState: ThemeState = {
// Available themes are defined in `themes.scss`
id: 'eighties',
numOfColors: 8,
timetableOrientation: HORIZONTAL,
showTitle: false,
};
export const themeIds = themes.map((obj: Theme) => obj.id);

function theme(state: ThemeState = defaultThemeState, action: Actions): ThemeState {
function setTheme(newTheme: string): ThemeState {
function setTheme(newTheme: Theme): ThemeState {
// Update theme analytics info
withTracker((tracker) => tracker.setCustomDimension(DIMENSIONS.theme, newTheme));
withTracker((tracker) => tracker.setCustomDimension(DIMENSIONS.theme, newTheme.id));

return {
...state,
id: newTheme,
id: newTheme.id,
numOfColors: newTheme.numOfColors,
Copy link
Member

@zwliew zwliew Aug 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this affects local storage schema, we should be more careful here. In the long run, I don't think we'll want to store numOfColors in ThemeState as it only contains user customizable data (i.e. current theme ID, should show title, orientation of timetable).

Since numOfColors is a static property of a theme, instead of storing it in Redux, we should store it in a predefined readonly map like the one that you've defined in themes.json. This has the added benefit of you no longer having to pass numOfColors around everywhere as you can just read it from the map.

That said, if/when we add user-defined themes, they should be stored in Redux, but likely in a different place instead of ThemeState.

};
}

switch (action.type) {
case SELECT_THEME:
// Reassign all modules' color when changing theme
return setTheme(action.payload);

case CYCLE_THEME: {
const newThemeIndex =
(themeIds.indexOf(state.id) + themeIds.length + action.payload) % themeIds.length;

return setTheme(themeIds[newThemeIndex]);
return setTheme(themes[newThemeIndex]);
}
case TOGGLE_TIMETABLE_ORIENTATION:
return {
Expand Down
5 changes: 5 additions & 0 deletions website/src/reducers/timetables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { TimetablesState } from 'types/reducers';
import config from 'config';
import { PersistConfig } from 'redux-persist/es/types';

// use 8 different colors for testing
const NUM_DIFFERENT_COLORS = 8;

const initialState = defaultTimetableState;

jest.mock('config');
Expand All @@ -25,6 +28,7 @@ describe('color reducers', () => {
semester: 1,
moduleCode: 'CS1010S',
moduleLessonConfig: {},
numOfColors: NUM_DIFFERENT_COLORS,
},
}).colors,
).toHaveProperty('1.CS1010S');
Expand All @@ -36,6 +40,7 @@ describe('color reducers', () => {
semester: 2,
moduleCode: 'CS3216',
moduleLessonConfig: {},
numOfColors: NUM_DIFFERENT_COLORS,
},
}).colors,
).toHaveProperty('2.CS3216');
Expand Down
24 changes: 21 additions & 3 deletions website/src/reducers/timetables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createMigrate } from 'redux-persist';
import { PersistConfig } from 'storage/persistReducer';
import { ModuleCode } from 'types/modules';
import { ModuleLessonConfig, SemTimetableConfig } from 'types/timetables';
import { ColorMapping, TimetablesState } from 'types/reducers';
import { ColorMapping, SemesterColorMap, TimetablesState } from 'types/reducers';

import config from 'config';
import {
Expand All @@ -14,6 +14,7 @@ import {
HIDE_LESSON_IN_TIMETABLE,
REMOVE_MODULE,
SELECT_MODULE_COLOR,
REASSIGN_ALL_MODULES_COLOR,
SET_LESSON_CONFIG,
SET_TIMETABLE,
SHOW_LESSON_IN_TIMETABLE,
Expand Down Expand Up @@ -131,7 +132,7 @@ function semColors(state: ColorMapping = defaultSemColorMap, action: Actions): C
case ADD_MODULE:
return {
...state,
[moduleCode]: getNewColor(values(state)),
[moduleCode]: getNewColor(values(state), action.payload.numOfColors),
};

case REMOVE_MODULE:
Expand All @@ -148,6 +149,19 @@ function semColors(state: ColorMapping = defaultSemColorMap, action: Actions): C
}
}

function recolor(state: SemesterColorMap, numOfColors: number): SemesterColorMap {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor thought: This is a fine approach, but I wonder if it'd be better to remove this function and leave color indices as is. Instead, we could modulo the colorIndex when rendering colors. I doubt it makes any difference though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I used an action to reassign the colors is that there are too many places where we have to modulo the colorIndex when rendering colors, which might make code changes hard to keep track of.

const newSemesterColorMap: SemesterColorMap = {};
joeng03 marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(state).forEach((semester) => {
const colorMapping = state[semester];
joeng03 marked this conversation as resolved.
Show resolved Hide resolved
const newColorMapping: ColorMapping = {};
Object.keys(colorMapping).forEach((moduleCode) => {
newColorMapping[moduleCode] = colorMapping[moduleCode] % numOfColors;
joeng03 marked this conversation as resolved.
Show resolved Hide resolved
});
newSemesterColorMap[semester] = newColorMapping;
});
return newSemesterColorMap;
}

// Map of semester to list of hidden modules
const defaultHiddenState: ModuleCode[] = [];
function semHiddenModules(state = defaultHiddenState, action: Actions) {
Expand Down Expand Up @@ -182,7 +196,6 @@ function timetables(
if (!action.payload) {
return state;
}

joeng03 marked this conversation as resolved.
Show resolved Hide resolved
switch (action.type) {
case SET_TIMETABLE: {
const { semester, timetable, colors } = action.payload;
Expand Down Expand Up @@ -219,6 +232,11 @@ function timetables(
hidden: { [semester]: hidden },
};
}
case REASSIGN_ALL_MODULES_COLOR:
return {
...state,
colors: recolor(state.colors, action.payload.numOfColors),
};

default:
return state;
Expand Down
12 changes: 12 additions & 0 deletions website/src/styles/constants.scss
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ $nusmods-theme-colors: (
#b6b3eb,
#bc9458
),
tequila: (
#e7553e,
#ff9f1e,
#ffc7c7,
#f1baa1,
#ffd700,
#8febdb,
#a9daf5,
#80b9f3,
#b9848c,
#b249f2,
),
tomorrow: (
#c66,
#de935f,
Expand Down
1 change: 1 addition & 0 deletions website/src/types/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const HORIZONTAL: TimetableOrientation = 'HORIZONTAL';

export type ThemeState = Readonly<{
id: string;
numOfColors: number;
timetableOrientation: TimetableOrientation;
showTitle: boolean;
}>;
Expand Down
1 change: 1 addition & 0 deletions website/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type ThemeId = string;
export type Theme = {
readonly id: ThemeId;
readonly name: string;
readonly numOfColors: number;
};

export type Mode = 'LIGHT' | 'DARK';
Expand Down
Loading