Skip to content

Commit a3e9ad5

Browse files
authored
Migrate settings to Tanstack Query (#1040)
* Install TankStack library with corresponding devtool, integrate into App.tsx * Integrate react query in to setting page * Connect settings.useSquareEdge,useDarkMode,preferredTheme with FE components * Remove use of QueryContext to direct suspense query calling to avoid re-rendering of whole App component * Update all settings to use React Query * Fix FE build error by removing removed AppContext states on components * Remove redudant comments * Add Error Boundary for handling unintented error, resolve packages version and dependecies issue * Remove unwanted comments and resolve naming issues * Resolve setting item destructing before used, and other dependecies issues * Migrate JS fetch to Axios * Remove unwanted library import * Update ErrorBoundary to handle any unknown errors * Resolve build error * Rebase from server-rewrite * Rebase from server-rewrite, with autofix eslint * Refactor Settings component to fit the feat of profile picture * Remove unneccssary commnets * Resolved dropcard shadow logic with react query * Resolve CI build error and lint warnings from drag.ts * Resolve review errors from lucas * Resolve use-prefix issue for settings parameter with minor changes * Resolve review comments from mark * Update the prisma schema for user settings * Update the prisma schema for user settings * Resolve naming error
1 parent 1d6a6ef commit a3e9ad5

File tree

44 files changed

+2586
-1585
lines changed

Some content is hidden

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

44 files changed

+2586
-1585
lines changed

client/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
"@sentry/browser": "9.38.0",
3131
"@sentry/node": "9.38.0",
3232
"@sentry/react": "9.38.0",
33+
"@tanstack/react-query": "5.84.1",
3334
"@uiw/react-color": "2.7.1",
35+
"axios": "1.11.0",
3436
"clsx": "2.1.1",
3537
"colorizr": "3.0.8",
3638
"date-fns": "2.30.0",
@@ -78,6 +80,7 @@
7880
"@eslint/js": "9.31.0",
7981
"@tailwindcss/postcss": "4.1.11",
8082
"@tanstack/eslint-plugin-query": "5.83.1",
83+
"@tanstack/react-query-devtools": "5.84.1",
8184
"@types/file-saver": "2.0.7",
8285
"@types/is-base64": "1.1.3",
8386
"@types/lodash-es": "4.17.12",

client/pnpm-lock.yaml

Lines changed: 1975 additions & 1150 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/App.tsx

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { styled } from '@mui/system';
33
import { LocalizationProvider } from '@mui/x-date-pickers';
44
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
55
import * as Sentry from '@sentry/react';
6-
import React, { useContext, useEffect } from 'react';
6+
import React, { useContext, useEffect, useMemo } from 'react';
77
import { Outlet } from 'react-router-dom';
88

99
import getCourseInfo from './api/getCourseInfo';
1010
import getCoursesList from './api/getCoursesList';
11+
import { useGetUserSettingsQuery } from './api/user/queries';
1112
import T3SelectGif from './assets/T3-select.gif';
1213
import Alerts from './components/Alerts';
1314
import Controls from './components/controls/Controls';
@@ -18,7 +19,7 @@ import Sidebar from './components/sidebar/Sidebar';
1819
import Sponsors from './components/Sponsors';
1920
import Timetable from './components/timetable/Timetable';
2021
import { TimetableTabs } from './components/timetableTabs/TimetableTabs';
21-
import { contentPadding, rightContentPadding, themes } from './constants/theme';
22+
import { contentPadding, darkTheme, lightTheme, rightContentPadding } from './constants/theme';
2223
import {
2324
daysLong,
2425
getAvailableTermDetails,
@@ -98,17 +99,6 @@ const ICSButton = styled(Button)`
9899

99100
const App: React.FC = () => {
100101
const {
101-
themeObject,
102-
currentTheme,
103-
setCurrentTheme,
104-
is12HourMode,
105-
isDarkMode,
106-
isSquareEdges,
107-
isShowOnlyOpenClasses,
108-
isDefaultUnscheduled,
109-
isHideClassInfo,
110-
isHideExamClasses,
111-
isConvertToLocalTimezone,
112102
setAlertMsg,
113103
setErrorVisibility,
114104
days,
@@ -144,7 +134,9 @@ const App: React.FC = () => {
144134
setAssignedColors,
145135
} = useContext(CourseContext);
146136

147-
const decodedAssignedColors = useColorsDecoder(assignedColors);
137+
const { preferredTheme, isDarkMode, unscheduleClassesByDefault, convertToLocalTimezone } = useGetUserSettingsQuery();
138+
139+
const decodedAssignedColors = useColorsDecoder(assignedColors, preferredTheme);
148140

149141
setDropzoneRange(days.length, earliestStartTime, latestEndTime);
150142

@@ -276,7 +268,7 @@ const App: React.FC = () => {
276268

277269
// null means a class is unscheduled
278270
Object.keys(course.activities).forEach((activity) => {
279-
prev[course.code][activity] = isDefaultUnscheduled
271+
prev[course.code][activity] = unscheduleClassesByDefault
280272
? null
281273
: (course.activities[activity].find((x) => x.enrolments !== x.capacity && x.periods.length) ??
282274
course.activities[activity].find((x) => x.periods.length) ??
@@ -302,7 +294,7 @@ const App: React.FC = () => {
302294
const codes: string[] = Array.isArray(data) ? data : [data];
303295
Promise.all(
304296
codes.map((code) =>
305-
getCourseInfo(term.substring(0, 2), code, term.substring(2), isConvertToLocalTimezone).catch((err) => {
297+
getCourseInfo(term.substring(0, 2), code, term.substring(2), convertToLocalTimezone).catch((err) => {
306298
return err;
307299
}),
308300
),
@@ -440,7 +432,7 @@ const App: React.FC = () => {
440432

441433
useEffect(() => {
442434
updateTimetableEvents();
443-
}, [year, isConvertToLocalTimezone]);
435+
}, [year, convertToLocalTimezone]);
444436

445437
// The following three useUpdateEffects update local storage whenever a change is made to the timetable
446438
useUpdateEffect(() => {
@@ -505,8 +497,8 @@ const App: React.FC = () => {
505497
* Upon switching timetable, reset default bounds
506498
*/
507499
useEffect(() => {
508-
setEarliestStartTime(getDefaultStartTime(isConvertToLocalTimezone));
509-
setLatestEndTime(getDefaultEndTime(isConvertToLocalTimezone));
500+
setEarliestStartTime(getDefaultStartTime(convertToLocalTimezone));
501+
setLatestEndTime(getDefaultEndTime(convertToLocalTimezone));
510502
}, [selectedTimetable]);
511503

512504
/**
@@ -517,7 +509,7 @@ const App: React.FC = () => {
517509
Math.min(
518510
...selectedCourses.map((course) => course.earliestStartTime),
519511
...Object.entries(createdEvents).map(([_, eventPeriod]) => Math.floor(eventPeriod.time.start)),
520-
getDefaultStartTime(isConvertToLocalTimezone),
512+
getDefaultStartTime(convertToLocalTimezone),
521513
prev,
522514
),
523515
);
@@ -526,7 +518,7 @@ const App: React.FC = () => {
526518
Math.max(
527519
...selectedCourses.map((course) => course.latestFinishTime),
528520
...Object.entries(createdEvents).map(([_, eventPeriod]) => Math.ceil(eventPeriod.time.end)),
529-
getDefaultEndTime(isConvertToLocalTimezone),
521+
getDefaultEndTime(convertToLocalTimezone),
530522
prev,
531523
),
532524
);
@@ -546,50 +538,12 @@ const App: React.FC = () => {
546538

547539
useUpdateEffect(() => {
548540
updateTimetableDaysAndTimes();
549-
}, [createdEvents, selectedCourses, isConvertToLocalTimezone]);
550-
551-
useEffect(() => {
552-
storage.set('currentTheme', currentTheme);
553-
}, [currentTheme]);
554-
555-
useEffect(() => {
556-
storage.set('is12HourMode', is12HourMode);
557-
}, [is12HourMode]);
558-
559-
useEffect(() => {
560-
storage.set('isDarkMode', isDarkMode);
561-
}, [isDarkMode]);
562-
563-
useEffect(() => {
564-
storage.set('isSquareEdges', isSquareEdges);
565-
}, [isSquareEdges]);
566-
567-
useEffect(() => {
568-
storage.set('isShowOnlyOpenClasses', isShowOnlyOpenClasses);
569-
}, [isShowOnlyOpenClasses]);
541+
}, [createdEvents, selectedCourses, convertToLocalTimezone]);
570542

571-
useEffect(() => {
572-
storage.set('isDefaultUnscheduled', isDefaultUnscheduled);
573-
}, [isDefaultUnscheduled]);
574-
575-
useEffect(() => {
576-
storage.set('isHideClassInfo', isHideClassInfo);
577-
}, [isHideClassInfo]);
578-
579-
useEffect(() => {
580-
storage.set('isHideExamClasses', isHideExamClasses);
581-
}, [isHideExamClasses]);
582-
583-
useEffect(() => {
584-
storage.set('isConvertToLocalTimezone', isConvertToLocalTimezone);
585-
}, [isConvertToLocalTimezone]);
586-
587-
// Validate the currentTheme
588-
useEffect(() => {
589-
if (!Object.keys(themes).includes(currentTheme)) {
590-
setCurrentTheme(Object.keys(themes)[0]);
591-
}
592-
}, [currentTheme]);
543+
const themeObject = useMemo(
544+
() => (isDarkMode ? darkTheme(preferredTheme) : lightTheme(preferredTheme)),
545+
[isDarkMode, preferredTheme],
546+
);
593547

594548
const globalStyle = {
595549
body: {

client/src/api/timetable/mutations.ts

Whitespace-only changes.

client/src/api/timetable/queries.ts

Whitespace-only changes.

client/src/api/timetable/routes.ts

Whitespace-only changes.

client/src/api/user/mutations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
3+
import type { UserSettings } from '../../interfaces/User';
4+
import { setUserSettings } from './routes';
5+
6+
export const useSetUserSettings = () =>
7+
useMutation({
8+
mutationFn: (settings: Partial<UserSettings>) => setUserSettings(settings),
9+
meta: {
10+
invalidatesQuery: ['settings'],
11+
},
12+
}).mutate;

client/src/api/user/queries.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useSuspenseQuery } from '@tanstack/react-query';
2+
3+
import { getUserSettings } from './routes';
4+
5+
export const useGetUserSettingsQuery = () =>
6+
useSuspenseQuery({
7+
queryKey: ['settings'],
8+
queryFn: getUserSettings,
9+
}).data;

client/src/api/user/routes.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import axios from 'axios';
2+
3+
import { UserInfo, UserSettings } from '../../interfaces/User';
4+
import { API_URL } from '../config';
5+
6+
const apiClient = axios.create({
7+
baseURL: API_URL.server,
8+
withCredentials: true,
9+
headers: {
10+
'Content-Type': 'application/json',
11+
},
12+
});
13+
14+
export const getUserProfile = async (): Promise<UserInfo> => {
15+
return (await apiClient.get('/user/profile')).data;
16+
};
17+
18+
export const getUserSettings = async (): Promise<UserSettings> => {
19+
return (await apiClient.get('/user/settings')).data;
20+
};
21+
22+
export const setUserSettings = async (settings: Partial<UserSettings>): Promise<void> => {
23+
await apiClient.post('/user/settings', settings);
24+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import PageError from './ErrorPage/ErrorPage';
4+
5+
interface ErrorBoundaryProps {
6+
children: React.ReactNode;
7+
}
8+
9+
interface ErrorBoundaryState {
10+
hasError: boolean;
11+
error: Error;
12+
}
13+
14+
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
15+
state: ErrorBoundaryState = { hasError: false, error: new Error('Uknown error') };
16+
17+
static getDerivedStateFromError(error: Error) {
18+
return { hasError: true, error: error };
19+
}
20+
21+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
22+
console.error('Error caught by ErrorBoundary:', error, errorInfo);
23+
}
24+
25+
render() {
26+
if (this.state.hasError) {
27+
return <PageError errorStack={this.state.error} />;
28+
}
29+
30+
return this.props.children;
31+
}
32+
}
33+
34+
export default ErrorBoundary;

0 commit comments

Comments
 (0)