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

feature/31 create graphical error notifications #118

Merged
merged 7 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 23 additions & 3 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import styled, { ThemeProvider } from 'styled-components';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

import { MuiThemeProvider, Box } from '@material-ui/core';
import { MuiThemeProvider, Box, Snackbar } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import { CourseData, ClassData, filterOutClasses } from '@notangles/common';
Expand All @@ -18,6 +19,7 @@ import useColorMapper from './hooks/useColorMapper';
import storage from './utils/storage';

import { darkTheme, lightTheme } from './constants/theme';
import NetworkError from './interfaces/NetworkError';

const StyledApp = styled(Box)`
height: 100%;
Expand Down Expand Up @@ -68,6 +70,8 @@ const App: FunctionComponent = () => {
const [selectedClasses, setSelectedClasses] = useState<ClassData[]>([]);
const [is12HourMode, setIs12HourMode] = useState<boolean>(storage.get('is12HourMode'));
const [isDarkMode, setIsDarkMode] = useState<boolean>(storage.get('isDarkMode'));
const [errorMsg, setErrorMsg] = useState<String>('');
const [errorVisibility, setErrorVisibility] = useState<boolean>(false);

const assignedColors = useColorMapper(
selectedCourses.map((course) => course.code),
Expand Down Expand Up @@ -102,11 +106,16 @@ const App: FunctionComponent = () => {
};

const handleSelectCourse = async (courseCode: string) => {
const selectedCourseClasses = await getCourseInfo('2020', 'T3', courseCode);
if (selectedCourseClasses) {
try {
const selectedCourseClasses = await getCourseInfo('2020', 'T3', courseCode);
const newSelectedCourses = [...selectedCourses, selectedCourseClasses];
populateTimetable(selectedCourseClasses); // TODO: temp until auto-timetabling is done
setSelectedCourses(newSelectedCourses);
} catch (e) {
if (e instanceof NetworkError) {
setErrorMsg(e.message);
setErrorVisibility(true);
}
}
};

Expand All @@ -120,6 +129,10 @@ const App: FunctionComponent = () => {
));
};

const handleErrorClose = () => {
setErrorVisibility(false);
};

return (
<MuiThemeProvider theme={isDarkMode ? darkTheme : lightTheme}>
<ThemeProvider theme={isDarkMode ? darkTheme : lightTheme}>
Expand All @@ -138,6 +151,8 @@ const App: FunctionComponent = () => {
assignedColors={assignedColors}
handleSelect={handleSelectCourse}
handleRemove={handleRemoveCourse}
setErrorMsg={setErrorMsg}
setErrorVisibility={setErrorVisibility}
/>
</SelectWrapper>
</Grid>
Expand Down Expand Up @@ -171,6 +186,11 @@ const App: FunctionComponent = () => {
GitHub
</Link>
</Footer>
<Snackbar open={errorVisibility} autoHideDuration={6000} onClose={handleErrorClose}>
<Alert severity="error" onClose={handleErrorClose} variant="filled">
{errorMsg}
</Alert>
</Snackbar>
</Content>
</ContentWrapper>
</StyledApp>
Expand Down
20 changes: 14 additions & 6 deletions client/src/api/getCourseInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { CourseData } from '@notangles/common';
import { DbCourse, dbCourseToCourseData } from '../interfaces/DbCourse';
import { API_URL } from './config';
import NetworkError from '../interfaces/NetworkError';
import timeoutPromise from '../utils/timeoutPromise';
import TimeoutError from '../interfaces/TimeoutError';

/**
* Fetches the information of a specified course
Expand All @@ -18,19 +21,24 @@ const getCourseInfo = async (
year: string,
term: string,
courseCode: string,
): Promise<CourseData | null> => {
): Promise<CourseData> => {
const baseURL = `${API_URL}/terms/${year}-${term}`;
try {
const data = await fetch(`${baseURL}/courses/${courseCode}/`);
const data = await timeoutPromise(1000, fetch(`${baseURL}/courses/${courseCode}/`));
if (data.status === 400) {
throw new NetworkError('Internal server error');
}
const json: DbCourse = await data.json();
if (!json) {
throw Error('Fetch did not get results');
throw new NetworkError('Internal server error');
}

return dbCourseToCourseData(json);
} catch (error) {
console.error(error);
return null;
if (error instanceof TimeoutError) {
throw new NetworkError('Could not connect to server');
} else {
throw error;
}
}
};

Expand Down
19 changes: 14 additions & 5 deletions client/src/api/getCoursesList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { CoursesList } from '../interfaces/CourseOverview';
import { API_URL } from './config';
import NetworkError from '../interfaces/NetworkError';
import timeoutPromise from '../utils/timeoutPromise';
import TimeoutError from '../interfaces/TimeoutError';

interface FetchedCourse {
_id: string;
Expand Down Expand Up @@ -29,14 +32,20 @@ const toCoursesList = (data: FetchedCourse[]): CoursesList => (
const getCoursesList = async (
year: string,
term: string,
): Promise<CoursesList | null> => {
): Promise<CoursesList> => {
const baseURL = `${API_URL}/terms/${year}-${term}`;
try {
const res = await fetch(`${baseURL}/courses/`);
return toCoursesList(await res.json());
const data = await timeoutPromise(1000, fetch(`${baseURL}/courses/`));
if (data.status === 400) {
throw new NetworkError('Internal server error');
maxwowo marked this conversation as resolved.
Show resolved Hide resolved
}
return toCoursesList(await data.json());
} catch (error) {
console.error(error);
return null;
if (error instanceof TimeoutError) {
throw new NetworkError('Could not connect to server');
} else {
throw error;
}
}
};

Expand Down
18 changes: 14 additions & 4 deletions client/src/components/CourseSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import styled, { css } from 'styled-components';
import { CourseData } from '@notangles/common';
import { CoursesList, CourseOverview } from '../interfaces/CourseOverview';
import getCoursesList from '../api/getCoursesList';
import NetworkError from '../interfaces/NetworkError';

const SEARCH_DELAY = 300;

Expand Down Expand Up @@ -54,7 +55,7 @@ const StyledSelect = styled(Box)`
left: -1px;
`;

const StyledTextField = styled(TextField)<{
const StyledTextField = styled(TextField) <{
theme: Theme
}>`
.MuiOutlinedInput-root {
Expand Down Expand Up @@ -83,7 +84,7 @@ const StyledInputAdornment = styled(InputAdornment)`

const StyledChip = styled(Chip).withConfig({
shouldForwardProp: (prop) => !['backgroundColor'].includes(prop),
})<{
}) <{
backgroundColor: string
}>`
transition: none !important;
Expand Down Expand Up @@ -123,13 +124,17 @@ interface CourseSelectProps {
assignedColors: Record<string, string>
handleSelect(courseCode: string): void
handleRemove(courseCode: string): void
setErrorMsg(errorMsg: string): void
setErrorVisibility(visibility: boolean): void
}

const CourseSelect: React.FC<CourseSelectProps> = ({
selectedCourses,
assignedColors,
handleSelect,
handleRemove,
setErrorMsg,
setErrorVisibility,
}) => {
const [coursesList, setCoursesList] = useState<CoursesList>([]);
const [options, setOptions] = useState<CoursesList>([]);
Expand Down Expand Up @@ -253,10 +258,15 @@ const CourseSelect: React.FC<CourseSelectProps> = ({
};

const fetchCoursesList = async () => {
const fetchedCoursesList = await getCoursesList('2020', 'T3');
if (fetchedCoursesList) {
try {
const fetchedCoursesList = await getCoursesList('2020', 'T3');
setCoursesList(fetchedCoursesList);
fuzzy = new Fuse(fetchedCoursesList, searchOptions);
} catch (e) {
if (e instanceof NetworkError) {
setErrorMsg(e.message);
setErrorVisibility(true);
}
}
};

Expand Down
5 changes: 5 additions & 0 deletions client/src/interfaces/NetworkError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default class NetworkError extends Error {
constructor(message = '') {
super(message);
}
}
19 changes: 19 additions & 0 deletions client/src/utils/timeoutPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import TimeoutError from '../interfaces/TimeoutError';

export default function timeoutPromise(ms: number, promise: Promise<Response>): Promise<Response> {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new TimeoutError('timeout'));
}, ms);
promise.then(
(res) => {
clearTimeout(timeoutId);
resolve(res);
},
(err) => {
clearTimeout(timeoutId);
reject(err);
},
);
});
}