diff --git a/src/features/sicp/errors/SicpErrorBoundary.tsx b/src/features/sicp/errors/SicpErrorBoundary.tsx new file mode 100644 index 0000000000..771d2685d3 --- /dev/null +++ b/src/features/sicp/errors/SicpErrorBoundary.tsx @@ -0,0 +1,36 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; + +import getSicpError, { SicpErrorType } from './SicpErrors'; + +type Props = { + children: ReactNode; +}; + +type State = { + hasError: boolean; +}; + +class SicpErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(_: Error): State { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return getSicpError(SicpErrorType.UNEXPECTED_ERROR); + } + + return this.props.children; + } +} + +export default SicpErrorBoundary; diff --git a/src/features/sicp/errors/SicpErrors.tsx b/src/features/sicp/errors/SicpErrors.tsx new file mode 100644 index 0000000000..a4f67b533d --- /dev/null +++ b/src/features/sicp/errors/SicpErrors.tsx @@ -0,0 +1,58 @@ +import { NonIdealState } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; + +export enum SicpErrorType { + UNEXPECTED_ERROR, + PAGE_NOT_FOUND_ERROR, + PARSING_ERROR +} + +const unexpectedError = ( +
+ Something unexpected went wrong trying to load this page. Please try refreshing the page. If the + issue persists, kindly let us know by filing an issue at{' '} + + https://github.com/source-academy/cadet-frontend + + . +
+); + +const pageNotFoundError = ( +
+ We could not find the page you were looking for. Please check the URL again. If you believe the + URL is correct, kindly let us know by filing an issue at{' '} + + https://github.com/source-academy/cadet-frontend + + . +
+); + +const parsingError = ( +
+ An error occured while loading the page. Kindly let us know by filing an issue at{' '} + + https://github.com/source-academy/cadet-frontend + {' '} + and we will get it fixed as soon as possible. +
+); + +const errorComponent = (description: JSX.Element) => ( + +); + +const getSicpError = (type: SicpErrorType) => { + switch (type) { + case SicpErrorType.PAGE_NOT_FOUND_ERROR: + return errorComponent(pageNotFoundError); + case SicpErrorType.PARSING_ERROR: + return errorComponent(parsingError); + default: + // handle unexpected error case + return errorComponent(unexpectedError); + } +}; + +export default getSicpError; diff --git a/src/features/sicp/errors/__tests__/SicpErrors.tsx b/src/features/sicp/errors/__tests__/SicpErrors.tsx new file mode 100644 index 0000000000..690623d94f --- /dev/null +++ b/src/features/sicp/errors/__tests__/SicpErrors.tsx @@ -0,0 +1,20 @@ +import { mount } from 'enzyme'; + +import getSicpError, { SicpErrorType } from '../SicpErrors'; + +describe('Sicp errors:', () => { + test('unexpected error renders correctly', () => { + const tree = mount(getSicpError(SicpErrorType.UNEXPECTED_ERROR)); + expect(tree.debug()).toMatchSnapshot(); + }); + + test('page not found error renders correctly', () => { + const tree = mount(getSicpError(SicpErrorType.PAGE_NOT_FOUND_ERROR)); + expect(tree.debug()).toMatchSnapshot(); + }); + + test('unexpected error renders correctly', () => { + const tree = mount(getSicpError(SicpErrorType.PARSING_ERROR)); + expect(tree.debug()).toMatchSnapshot(); + }); +}); diff --git a/src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap b/src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap new file mode 100644 index 0000000000..832e83e25a --- /dev/null +++ b/src/features/sicp/errors/__tests__/__snapshots__/SicpErrors.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Sicp errors: page not found error renders correctly 1`] = ` +" +
+
+ + + + + error + + + + + +
+ +

+ Something went wrong :( +

+
+
+ We could not find the page you were looking for. Please check the URL again. If you believe the URL is correct, kindly let us know by filing an issue at + + + https://github.com/source-academy/cadet-frontend + + . +
+
+
" +`; + +exports[`Sicp errors: unexpected error renders correctly 1`] = ` +" +
+
+ + + + + error + + + + + +
+ +

+ Something went wrong :( +

+
+
+ Something unexpected went wrong trying to load this page. Please try refreshing the page. If the issue persists, kindly let us know by filing an issue at + + + https://github.com/source-academy/cadet-frontend + + . +
+
+
" +`; + +exports[`Sicp errors: unexpected error renders correctly 2`] = ` +" +
+
+ + + + + error + + + + + +
+ +

+ Something went wrong :( +

+
+
+ An error occured while loading the page. Kindly let us know by filing an issue at + + + https://github.com/source-academy/cadet-frontend + + + and we will get it fixed as soon as possible. +
+
+
" +`; diff --git a/src/features/sicp/parser/ParseJson.tsx b/src/features/sicp/parser/ParseJson.tsx index e52dcc4af2..c46d48752f 100644 --- a/src/features/sicp/parser/ParseJson.tsx +++ b/src/features/sicp/parser/ParseJson.tsx @@ -42,7 +42,8 @@ const handleFootnote = (obj: JsonType, refs: React.MutableRefObject<{}>) => { return (
{obj['count'] === 1 &&
} -
(refs.current[obj['id']!] = ref)} className="sicp-footnote"> +
+
(refs.current[obj['id']!] = ref)} /> {'[' + obj['count'] + '] '} {parseArr(obj['child']!, refs)}
@@ -113,7 +114,8 @@ const handleSnippet = (obj: JsonType) => { }; const handleFigure = (obj: JsonType, refs: React.MutableRefObject<{}>) => ( -
(refs.current[obj['id']!] = ref)} className="sicp-figure"> +
+
(refs.current[obj['id']!] = ref)} /> {handleImage(obj, refs)} {obj['captionName'] && (
@@ -152,7 +154,8 @@ const handleTD = (obj: JsonType, refs: React.MutableRefObject<{}>, index: intege const handleExercise = (obj: JsonType, refs: React.MutableRefObject<{}>) => { return ( -
(refs.current[obj['id']!] = ref)}> +
+
(refs.current[obj['id']!] = ref)} /> ) => ( <> -
(refs.current[obj['id']!] = ref)} className="sicp-text"> +
+
(refs.current[obj['id']!] = ref)} /> {parseArr(obj['child']!, refs)}

diff --git a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap index 57fff34e74..1c5ea40bb2 100644 --- a/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap +++ b/src/features/sicp/parser/__tests__/__snapshots__/ParseJson.tsx.snap @@ -115,18 +115,21 @@ exports[`Parse epigraph EPIGRAPH with title successful 1`] = ` exports[`Parse exercise EXERCISE with solution successful 1`] = ` "
+
" `; exports[`Parse exercise EXERCISE without solution successful 1`] = ` "
+
" `; exports[`Parse figures FIGURE with image and scale successful 1`] = ` "
+
\\"id\\"
name @@ -141,6 +144,7 @@ exports[`Parse figures FIGURE with image and scale successful 1`] = ` exports[`Parse figures FIGURE with image successful 1`] = ` "
+
\\"id\\"
name @@ -155,6 +159,7 @@ exports[`Parse figures FIGURE with image successful 1`] = ` exports[`Parse figures FIGURE with snippet successful 1`] = ` "
+
name @@ -169,6 +174,7 @@ exports[`Parse figures FIGURE with snippet successful 1`] = ` exports[`Parse figures FIGURE with table successful 1`] = ` "
+
@@ -220,6 +226,7 @@ exports[`Parse footnote DISPLAYFOOTNOTE count is 1 successful 1`] = ` "

+
[1] @@ -235,6 +242,7 @@ exports[`Parse footnote DISPLAYFOOTNOTE count is 1 successful 1`] = ` exports[`Parse footnote DISPLAYFOOTNOTE count is 2 successful 1`] = ` "
+
[2] @@ -364,6 +372,7 @@ exports[`Parse section SECTION successful 1`] = `
+

Mock Text @@ -379,6 +388,7 @@ exports[`Parse section SECTION successful 1`] = `

+

Mock Text diff --git a/src/pages/sicp/Sicp.tsx b/src/pages/sicp/Sicp.tsx index 6e2f9384e7..0be777aa79 100644 --- a/src/pages/sicp/Sicp.tsx +++ b/src/pages/sicp/Sicp.tsx @@ -1,7 +1,6 @@ import 'katex/dist/katex.min.css'; import { Button, Classes, NonIdealState, Spinner } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; @@ -11,6 +10,8 @@ import { resetWorkspace, toggleUsingSubst } from 'src/commons/workspace/Workspac import { parseArr, ParseJsonError } from 'src/features/sicp/parser/ParseJson'; import { getNext, getPrev } from 'src/features/sicp/TableOfContentsHelper'; +import SicpErrorBoundary from '../../features/sicp/errors/SicpErrorBoundary'; +import getSicpError, { SicpErrorType } from '../../features/sicp/errors/SicpErrors'; import SicpIndexPage from './subcomponents/SicpIndexPage'; type SicpProps = RouteComponentProps<{}>; @@ -26,49 +27,30 @@ export const CodeSnippetContext = React.createContext({ const loadingComponent = } />; -const unexpectedError = ( -

- Something unexpected went wrong trying to load this page. Please try refreshing the page. If the - issue persists, kindly let us know by filing an issue at{' '} - - https://github.com/source-academy/cadet-frontend - - . -
-); -const pageNotFoundError = ( -
- We could not find the page you were looking for. Please check the URL again. If you believe the - URL is correct, kindly let us know by filing an issue at{' '} - - https://github.com/source-academy/cadet-frontend - - . -
-); -const parsingError = ( -
- An error occured while loading the page. Kindly let us know by filing an issue at{' '} - - https://github.com/source-academy/cadet-frontend - {' '} - and we will get it fixed as soon as possible. -
-); - -const errorComponent = (description: JSX.Element) => ( - -); - const Sicp: React.FC = props => { const [data, setData] = React.useState(<>); const [loading, setLoading] = React.useState(true); const [active, setActive] = React.useState('0'); const { section } = useParams<{ section: string }>(); const topRef = React.useRef(null); + const bottomRef = React.useRef(null); const refs = React.useRef({}); const history = useHistory(); + const scrollRefIntoView = (ref: HTMLDivElement | null) => { + if (!ref) { + return; + } + + // Hack to get scrolling to work properly. + // When 'block: start' option is used with scrollIntoView, the whole page scrolls with it. + // This issue does not occur when the option 'block: nearest' is used. + // To get `block: nearest` to mimic `block: start` behaviour, we first scroll to the bottom of + // the page before scrolling to the desired ref using the `block: nearest` option. + bottomRef.current!.scrollIntoView({ block: 'end' }); + ref.scrollIntoView({ block: 'nearest' }); + }; + // Fetch json data React.useEffect(() => { setLoading(true); @@ -95,38 +77,33 @@ const Sicp: React.FC = props => { } }) .catch(error => { - console.log(error); + console.error(error); if (error.message === 'Not Found') { // page not found - setData(errorComponent(pageNotFoundError)); + setData(getSicpError(SicpErrorType.PAGE_NOT_FOUND_ERROR)); } else if (error instanceof ParseJsonError) { // error occured while parsing JSON - setData(errorComponent(parsingError)); + setData(getSicpError(SicpErrorType.PARSING_ERROR)); } else { - setData(errorComponent(unexpectedError)); + setData(getSicpError(SicpErrorType.UNEXPECTED_ERROR)); } + setLoading(false); }); }, [section]); // Scroll to correct position React.useEffect(() => { - const hash = props.location.hash; - - if (!hash) { - if (topRef.current) { - topRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } + if (loading) { return; } + const hash = props.location.hash; const ref = refs.current[hash]; - if (ref) { - ref.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); - } - }, [props.location.hash]); + scrollRefIntoView(ref); + }, [props.location.hash, loading]); // Close all active code snippet when new page is loaded React.useEffect(() => { @@ -154,19 +131,22 @@ const Sicp: React.FC = props => { return (
- -
- {loading ? ( -
{loadingComponent}
- ) : section === 'index' ? ( - - ) : ( -
- {data} - {navigationButtons} -
- )} - + + +
+ {loading ? ( +
{loadingComponent}
+ ) : section === 'index' ? ( + + ) : ( +
+ {data} + {navigationButtons} +
+ )} +
+ +
); }; diff --git a/src/pages/sicp/subcomponents/SicpToc.tsx b/src/pages/sicp/subcomponents/SicpToc.tsx index 7664b25da7..ea21f81b07 100644 --- a/src/pages/sicp/subcomponents/SicpToc.tsx +++ b/src/pages/sicp/subcomponents/SicpToc.tsx @@ -2,7 +2,7 @@ import { Tree, TreeNodeInfo } from '@blueprintjs/core'; import { cloneDeep } from 'lodash'; import * as React from 'react'; import { useState } from 'react'; -import { Redirect } from 'react-router'; +import { useHistory } from 'react-router'; import toc from '../../../features/sicp/data/toc.json'; @@ -17,7 +17,7 @@ type OwnProps = { */ const SicpToc: React.FC = props => { const [sidebarContent, setSidebarContent] = useState(toc as TreeNodeInfo[]); - const [slug, setSlug] = useState(''); + const history = useHistory(); const handleNodeExpand = (_node: TreeNodeInfo, path: integer[]) => { const newState = cloneDeep(sidebarContent); @@ -36,14 +36,13 @@ const SicpToc: React.FC = props => { if (props.handleCloseToc) { props.handleCloseToc(); } - setSlug(String(node.nodeData)); + history.push('/interactive-sicp/' + String(node.nodeData)); }, - [props] + [history, props] ); return (
- {slug !== '' && }