Skip to content

Commit

Permalink
feature/31 create graphical error notifications (#118)
Browse files Browse the repository at this point in the history
* created graphical error notifications

* changed NetworkError to a custom error, fixed a bug where text disappears just before the notification dissapears and changed snackbar design for higher contrast with background

* Fixed a bug where 'Internal server error' never gets called

* added TimeoutError

Co-authored-by: itsViggo <vigo@192-168-1-108.tpgi.com.au>
  • Loading branch information
itsViggo and itsViggo committed Jul 21, 2020
1 parent 31d4c10 commit 742360a
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 18 deletions.
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');
}
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);
},
);
});
}

0 comments on commit 742360a

Please sign in to comment.