Skip to content

Commit 6d271bd

Browse files
authored
Add error view to Optimiser (#4074)
* feat: add ApiError view to optimiser * feat: add prompt text to error view * fix: make lesson slot nullable
1 parent 76f3cc5 commit 6d271bd

File tree

4 files changed

+136
-152
lines changed

4 files changed

+136
-152
lines changed

website/src/apis/optimiser.ts

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import axios from 'axios';
1+
import axios, { AxiosError } from 'axios';
22

33
const api = '/api/optimiser/optimise';
44

5-
interface OptimiseRequest {
5+
export interface OptimiseRequest {
66
modules: string[];
77
acadYear: string;
88
acadSem: number;
@@ -14,38 +14,30 @@ interface OptimiseRequest {
1414
lunchEnd: string;
1515
}
1616

17-
interface OptimiseResponse {
17+
export interface LessonSlot {
18+
DayIndex: number;
19+
EndMin: number;
20+
LessonKey: string | undefined;
21+
StartMin: number;
22+
classNo: string;
23+
coordinates: { x: number; y: number };
24+
day: string;
25+
endTime: string;
26+
lessonType: string;
27+
startTime: string;
28+
venue: string;
29+
}
30+
31+
export interface OptimiseResponse {
1832
shareableLink?: string;
19-
Assignments?: any;
20-
DaySlots?: any[][];
33+
Assignments?: { [lesson: string]: string };
34+
DaySlots?: (LessonSlot | null)[][];
35+
36+
// TODO: implement type
2137
[key: string]: any;
2238
}
2339

2440
export const sendOptimiseRequest = async (
25-
modules: string[],
26-
acadYear: string,
27-
acadSem: number,
28-
freeDays: string[],
29-
earliestTime: string,
30-
latestTime: string,
31-
recordings: string[],
32-
lunchStart: string,
33-
lunchEnd: string,
34-
): Promise<OptimiseResponse | null> => {
35-
const requestData: OptimiseRequest = {
36-
modules,
37-
acadYear,
38-
acadSem,
39-
freeDays,
40-
earliestTime,
41-
latestTime,
42-
recordings,
43-
lunchStart,
44-
lunchEnd,
45-
};
46-
47-
return axios
48-
.post<OptimiseResponse>(api, requestData)
49-
.then((resp) => resp.data)
50-
.catch(() => null);
51-
};
41+
params: OptimiseRequest,
42+
): Promise<OptimiseResponse | AxiosError> =>
43+
axios.post<OptimiseResponse>(api, params).then((resp) => resp.data);

website/src/views/errors/ApiError.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import { breakpointUp } from 'utils/css';
77

88
import styles from './ErrorPage.scss';
99

10+
const defaultPromptText = 'This could be because your device is offline or NUSMods is down :(';
11+
1012
type Props = {
1113
retry?: () => void;
1214
dataName?: string;
15+
promptText?: string;
1316
};
1417

1518
export default class ApiError extends React.PureComponent<Props> {
@@ -30,7 +33,7 @@ export default class ApiError extends React.PureComponent<Props> {
3033
};
3134

3235
override render() {
33-
const { retry, dataName } = this.props;
36+
const { retry, dataName, promptText } = this.props;
3437
const message = dataName ? `We can't load the ${dataName}` : "We can't connect to NUSMods";
3538

3639
return (
@@ -46,7 +49,7 @@ export default class ApiError extends React.PureComponent<Props> {
4649
<span className={styles.expr}>Oh no...</span> {message}
4750
</h1>
4851

49-
<p>This could be because your device is offline or NUSMods is down :(</p>
52+
<p>{promptText ?? defaultPromptText}</p>
5053
{/* TODO: Remove hacky message after we figure out what is wrong with Elastic Search. */}
5154
{dataName === 'course information' && (
5255
<>
Lines changed: 40 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,56 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22
import classnames from 'classnames';
33
import { Zap } from 'react-feather';
4-
import { sendOptimiseRequest } from 'apis/optimiser';
54
import { FreeDayConflict, LessonOption } from './types';
65
import styles from './OptimiserButton.scss';
76

87
interface OptimiserButtonProps {
98
freeDayConflicts: FreeDayConflict[];
109
lessonOptions: LessonOption[];
11-
acadYear: string;
12-
activeSemester: number;
13-
selectedFreeDays: Set<string>;
14-
earliestTime: string;
15-
latestTime: string;
16-
recordings: string[];
17-
earliestLunchTime: string;
18-
latestLunchTime: string;
19-
timetable: Record<string, any>;
20-
setShareableLink: (shareableLink: string) => void;
21-
setUnAssignedLessons: (unAssignedLessons: LessonOption[]) => void;
10+
isOptimising: boolean;
11+
onClick: () => void;
2212
}
2313

2414
const OptimiserButton: React.FC<OptimiserButtonProps> = ({
2515
freeDayConflicts,
2616
lessonOptions,
27-
acadYear,
28-
activeSemester,
29-
selectedFreeDays,
30-
earliestTime,
31-
latestTime,
32-
recordings,
33-
earliestLunchTime,
34-
latestLunchTime,
35-
timetable,
36-
setShareableLink,
37-
setUnAssignedLessons,
38-
}) => {
39-
const [isOptimising, setIsOptimising] = useState(false);
40-
41-
const optimiseTimetable = async () => {
42-
try {
43-
setIsOptimising(true);
44-
setShareableLink(''); // Reset shareable link
45-
const modulesList = Object.keys(timetable);
46-
const acadYearFormatted = `${acadYear.split('/')[0]}-${acadYear.split('/')[1]}`;
47-
48-
const data = await sendOptimiseRequest(
49-
modulesList,
50-
acadYearFormatted,
51-
activeSemester,
52-
Array.from(selectedFreeDays),
53-
earliestTime,
54-
latestTime,
55-
recordings,
56-
earliestLunchTime,
57-
latestLunchTime,
58-
);
59-
60-
if (data && data.shareableLink) {
61-
setShareableLink(data.shareableLink);
62-
const assignedLessons = new Set<string>();
63-
64-
if (data.Assignments !== null && data.DaySlots) {
65-
data.DaySlots.forEach((day: any) => {
66-
day.forEach((slot: any) => {
67-
if (slot.LessonKey) {
68-
const moduleCode = slot.LessonKey.split('|')[0];
69-
const lessonType = slot.LessonKey.split('|')[1];
70-
assignedLessons.add(`${moduleCode} ${lessonType}`);
71-
}
72-
});
73-
});
74-
}
75-
76-
setUnAssignedLessons(
77-
lessonOptions.filter((lesson) => !assignedLessons.has(lesson.displayText)),
78-
);
79-
}
80-
} finally {
81-
setIsOptimising(false);
82-
}
83-
};
84-
85-
return (
86-
<div className={styles.optimizeButtonSection}>
87-
<button
88-
type="button"
89-
className={classnames(
90-
'btn',
91-
styles.optimizeButton,
92-
freeDayConflicts.length > 0 || isOptimising || lessonOptions.length === 0
93-
? styles.disabled
94-
: styles.enabled,
95-
{
96-
disabled: isOptimising || freeDayConflicts.length > 0 || lessonOptions.length === 0,
97-
},
98-
)}
99-
onClick={() => {
100-
optimiseTimetable();
101-
}}
102-
>
103-
{!isOptimising ? (
104-
<Zap
105-
size={20}
106-
fill={freeDayConflicts.length > 0 || lessonOptions.length === 0 ? '#69707a' : '#ff5138'}
107-
/>
108-
) : (
109-
<span className={styles.optimizeButtonSpinner}>
110-
{isOptimising && <div className={styles.grower} />}
111-
</span>
112-
)}
113-
{isOptimising ? 'Searching and optimising...' : 'Optimise Timetable'}
114-
</button>
115-
<div className={styles.estimateTime}>
116-
<div>estimated time:</div>
117-
<div className={styles.estimateTimeValue}>5s - 40s</div>
118-
</div>
17+
isOptimising,
18+
onClick,
19+
}) => (
20+
<div className={styles.optimizeButtonSection}>
21+
<button
22+
type="button"
23+
className={classnames(
24+
'btn',
25+
styles.optimizeButton,
26+
freeDayConflicts.length > 0 || isOptimising || lessonOptions.length === 0
27+
? styles.disabled
28+
: styles.enabled,
29+
{
30+
disabled: isOptimising || freeDayConflicts.length > 0 || lessonOptions.length === 0,
31+
},
32+
)}
33+
onClick={() => {
34+
onClick();
35+
}}
36+
>
37+
{!isOptimising ? (
38+
<Zap
39+
size={20}
40+
fill={freeDayConflicts.length > 0 || lessonOptions.length === 0 ? '#69707a' : '#ff5138'}
41+
/>
42+
) : (
43+
<span className={styles.optimizeButtonSpinner}>
44+
{isOptimising && <div className={styles.grower} />}
45+
</span>
46+
)}
47+
{isOptimising ? 'Searching and optimising...' : 'Optimise Timetable'}
48+
</button>
49+
<div className={styles.estimateTime}>
50+
<div>estimated time:</div>
51+
<div className={styles.estimateTimeValue}>5s - 40s</div>
11952
</div>
120-
);
121-
};
53+
</div>
54+
);
12255

12356
export default OptimiserButton;

website/src/views/optimiser/OptimiserContent.tsx

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { getSemesterTimetableColors, getSemesterTimetableLessons } from 'selecto
44
import { State } from 'types/state';
55
import { ColorMapping } from 'types/reducers';
66
import { getModuleTimetable } from 'utils/modules';
7+
import { LessonSlot, OptimiseRequest, OptimiseResponse, sendOptimiseRequest } from 'apis/optimiser';
78
import Title from 'views/components/Title';
9+
import { flatten } from 'lodash';
10+
import ApiError from 'views/errors/ApiError';
811
import styles from './OptimiserContent.scss';
912
import OptimiserHeader from './OptimiserHeader';
1013
import OptimiserForm from './OptimiserForm';
@@ -30,6 +33,10 @@ const OptimiserContent: React.FC = () => {
3033
const [shareableLink, setShareableLink] = useState<string>('');
3134
const [hasSaturday, setHasSaturday] = useState<boolean>(false);
3235

36+
// button
37+
const [isOptimising, setIsOptimising] = useState<boolean>(false);
38+
const [error, setError] = useState<Error | null>(null);
39+
3340
// Generate lesson options from current timetable
3441
const lessonOptions = useMemo(() => {
3542
const options: LessonOption[] = [];
@@ -159,6 +166,57 @@ const OptimiserContent: React.FC = () => {
159166
});
160167
}, []);
161168

169+
const buttonOnClick = async () => {
170+
setError(null);
171+
setShareableLink(''); // Reset shareable link
172+
173+
const modulesList = Object.keys(timetable);
174+
const acadYearFormatted = `${acadYear.split('/')[0]}-${acadYear.split('/')[1]}`;
175+
176+
const params: OptimiseRequest = {
177+
modules: modulesList,
178+
acadYear: acadYearFormatted,
179+
acadSem: activeSemester,
180+
freeDays: Array.from(selectedFreeDays),
181+
earliestTime,
182+
latestTime,
183+
recordings,
184+
lunchStart: earliestLunchTime,
185+
lunchEnd: latestLunchTime,
186+
};
187+
188+
sendOptimiseRequest(params)
189+
.then(parseData)
190+
.catch((e) => setError(e))
191+
.finally(() => setIsOptimising(false));
192+
};
193+
194+
const parseData = async (data: OptimiseResponse | null) => {
195+
const link = data?.shareableLink;
196+
if (!link) {
197+
return;
198+
}
199+
setShareableLink(link);
200+
201+
const daySlots = data.DaySlots ?? [];
202+
const assignedLessons = new Set(
203+
flatten(daySlots)
204+
.map((slot: LessonSlot | null) => {
205+
const lessonKey = slot?.LessonKey;
206+
if (!lessonKey) {
207+
return null;
208+
}
209+
const [moduleCode, lessonType] = lessonKey.split('|');
210+
return `${moduleCode} ${lessonType}`;
211+
})
212+
.filter((lesson) => !!lesson),
213+
);
214+
215+
setUnAssignedLessons(
216+
lessonOptions.filter((lesson) => !assignedLessons.has(lesson.displayText)),
217+
);
218+
};
219+
162220
const openOptimisedTimetable = () => {
163221
if (shareableLink) {
164222
window.open(shareableLink, '_blank');
@@ -195,19 +253,17 @@ const OptimiserContent: React.FC = () => {
195253
<OptimiserButton
196254
freeDayConflicts={freeDayConflicts}
197255
lessonOptions={lessonOptions}
198-
acadYear={acadYear}
199-
activeSemester={activeSemester}
200-
selectedFreeDays={selectedFreeDays}
201-
earliestTime={earliestTime}
202-
latestTime={latestTime}
203-
recordings={recordings}
204-
earliestLunchTime={earliestLunchTime}
205-
latestLunchTime={latestLunchTime}
206-
timetable={timetable}
207-
setShareableLink={setShareableLink}
208-
setUnAssignedLessons={setUnAssignedLessons}
256+
isOptimising={isOptimising}
257+
onClick={buttonOnClick}
209258
/>
210259

260+
{!!error && (
261+
<ApiError
262+
dataName="timetable optimiser"
263+
promptText="This feature is in Beta, so we would really appreciate your feedback! "
264+
/>
265+
)}
266+
211267
{/* Optimiser results */}
212268
<OptimiserResults
213269
shareableLink={shareableLink}

0 commit comments

Comments
 (0)