diff --git a/frontend/package.json b/frontend/package.json
index d8a728e6..c226d516 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -28,7 +28,7 @@
"@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.12",
"@reduxjs/toolkit": "^1.9.3",
- "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v1.15.2",
+ "codeforlife": "github:ocadotechnology/codeforlife-package-javascript#v1.15.3",
"country-list": "^2.3.0",
"formik": "^2.2.9",
"react": "^18.2.0",
diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx
index a00b9846..849ed33d 100644
--- a/frontend/src/app/router.tsx
+++ b/frontend/src/app/router.tsx
@@ -15,9 +15,7 @@ import HomeLearning from '../pages/homeLearning/HomeLearning';
import PrivacyNotice from '../pages/privacyNotice/PrivacyNotice';
import TermsOfUse from '../pages/termsOfUse/TermsOfUse';
import Newsletter from '../pages/newsletter/Newsletter';
-import Forbidden from '../pages/forbidden/Forbidden';
-import PageNotFound from '../pages/pageNotFound/PageNotFound';
-import InternalServerError from '../pages/internalServerError/InternalServerError';
+import Error from '../pages/error/Error';
import TeacherSchool from '../pages/teacherDashboard/TeacherSchool';
import TeacherClasses from '../pages/teacherDashboard/TeacherClasses';
import TeacherAccount from '../pages/teacherDashboard/account/TeacherAccount';
@@ -74,9 +72,13 @@ export const paths = _('', {
termsOfUse: _('/terms-of-use'),
newsletter: _('/newsletter'),
error: _('/error', {
- forbidden: _('/forbidden'),
- pageNotFound: _('/page-not-found'),
- internalServerError: _('/internal-server-error')
+ forbidden: _('/?type=forbidden'),
+ pageNotFound: _('/?type=pageNotFound'),
+ tooManyRequests: _('/?type=tooManyRequests', {
+ teacher: _('&userType=teacher'),
+ independent: _('&userType=independent')
+ }),
+ internalServerError: _('/?type=internalServerError')
}),
rapidRouter: _('/rapid-router'),
kurono: _('/kurono')
@@ -139,18 +141,9 @@ const router = createBrowserRouter([
path: paths.newsletter._,
element:
},
- // TODO: merge separate error pages into one page with multiple states.
- {
- path: paths.error.forbidden._,
- element:
- },
- {
- path: paths.error.pageNotFound._,
- element:
- },
{
- path: paths.error.internalServerError._,
- element:
+ path: paths.error._,
+ element:
},
{
path: paths.teacher.dashboard.school._,
diff --git a/frontend/src/components/BaseErrorPage.tsx b/frontend/src/components/BaseErrorPage.tsx
deleted file mode 100644
index 6b6f5372..00000000
--- a/frontend/src/components/BaseErrorPage.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React from 'react';
-import {
- Unstable_Grid2 as Grid,
- Stack,
- Typography,
- Button
-} from '@mui/material';
-
-import { Image, ImageProps } from 'codeforlife/lib/esm/components';
-
-import BasePage from '../pages/BasePage';
-import { paths } from '../app/router';
-import PageSection from './PageSection';
-
-export interface ErrorTemplateProps {
- header: string,
- subheader: string,
- body: string,
- img: Pick
-};
-
-const BaseErrorPage: React.FC = ({
- header, subheader, body, img
-}) => {
- return (
-
-
-
-
-
-
- {header}
-
-
- {subheader}
-
-
- {body}
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default BaseErrorPage;
diff --git a/frontend/src/pages/error/Error.tsx b/frontend/src/pages/error/Error.tsx
new file mode 100644
index 00000000..34268b52
--- /dev/null
+++ b/frontend/src/pages/error/Error.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import {
+ Unstable_Grid2 as Grid,
+ Stack,
+ Typography,
+ Button
+} from '@mui/material';
+
+import { Image } from 'codeforlife/lib/esm/components';
+import { SearchParams } from 'codeforlife/lib/esm/helpers';
+
+import { paths } from '../../app/router';
+import PageSection from '../../components/PageSection';
+import BasePage from '../BasePage';
+import ErrorProps, {
+ forbidden403,
+ pageNotFound404,
+ tooManyRequests429,
+ internalServerError500
+} from './ErrorProps';
+
+const Error: React.FC = () => {
+ const errorTypes = [
+ '403', 'forbidden',
+ '404', 'pageNotFound',
+ '429', 'tooManyRequests',
+ '500', 'internalServerError'
+ ] as const;
+
+ const userTypes = [
+ 'teacher',
+ 'independent'
+ ] as const;
+
+ let params = SearchParams.get<{
+ type: typeof errorTypes[number],
+ userType?: typeof userTypes[number]
+ }>({
+ type: {
+ validate: SearchParams.validate.inOptions(errorTypes)
+ },
+ userType: {
+ validate: SearchParams.validate.inOptions(userTypes),
+ isRequired: false
+ }
+ });
+
+ if (params === null || (
+ ['429', 'tooManyRequests'].includes(params.type) &&
+ params.userType === undefined
+ )) {
+ // Special case. Don't redirect to an error page - we're already here.
+ params = { type: 'internalServerError' };
+ }
+
+ let errorProps: ErrorProps;
+ switch (params.type) {
+ case '403':
+ case 'forbidden':
+ errorProps = forbidden403();
+ break;
+ case '404':
+ case 'pageNotFound':
+ errorProps = pageNotFound404();
+ break;
+ case '429':
+ case 'tooManyRequests':
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ errorProps = tooManyRequests429(params.userType!);
+ break;
+ case '500':
+ case 'internalServerError':
+ errorProps = internalServerError500();
+ break;
+ }
+
+ return (
+
+
+
+
+
+
+ {errorProps.header}
+
+
+ {errorProps.subheader}
+
+
+ {errorProps.body}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Error;
diff --git a/frontend/src/pages/error/ErrorProps.tsx b/frontend/src/pages/error/ErrorProps.tsx
new file mode 100644
index 00000000..a3b974bd
--- /dev/null
+++ b/frontend/src/pages/error/ErrorProps.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import {
+ Link
+} from '@mui/material';
+
+import { ImageProps } from 'codeforlife/lib/esm/components';
+
+import { paths } from '../../app/router';
+import KirstyImage from '../../images/kirsty.png';
+import NigelImage from '../../images/nigel.png';
+import DeeImage from '../../images/dee.png';
+import PhilImage from '../../images/phil.png';
+
+export default interface ErrorProps {
+ header: string;
+ subheader: string;
+ body: string | React.ReactElement;
+ imageProps: ImageProps;
+};
+
+export function forbidden403(): ErrorProps {
+ return {
+ header: 'Oi!',
+ subheader: 'Kirsty says you\'re not allowed there.',
+ body: 'Those pages belong to Kirsty. She won\'t let you in even if you ask nicely.',
+ imageProps: { alt: 'kirsty', src: KirstyImage }
+ };
+}
+
+export function pageNotFound404(): ErrorProps {
+ return {
+ header: 'Uh oh!',
+ subheader: 'Sorry, Nigel can\'t find the page you were looking for.',
+ body: 'This might be because you have entered a web address incorrectly or the page has moved.',
+ imageProps: { alt: 'nigel', src: NigelImage }
+ };
+}
+
+export function tooManyRequests429(
+ userType: 'teacher' | 'independent'
+): ErrorProps {
+ const resetPasswordHref = (userType === 'teacher')
+ ? paths.resetPassword.teacher._
+ : paths.resetPassword.independent._;
+
+ return {
+ header: 'Temporary lock out!',
+ subheader: 'Your account has been temporarily blocked as there were too many unsuccessful requests.',
+ body: <>If you wish to proceed, please reset your password. Alternatively, you will need to wait 24 hours for your account to be unlocked again.>,
+ imageProps: { alt: 'phil', src: PhilImage }
+ };
+}
+
+export function internalServerError500(): ErrorProps {
+ return {
+ header: 'Zap!',
+ subheader: 'Oh dear! Something technical has gone wrong.',
+ body: 'Dee will attempt to fix this soon.',
+ imageProps: { alt: 'dee', src: DeeImage }
+ };
+}
diff --git a/frontend/src/pages/forbidden/Forbidden.tsx b/frontend/src/pages/forbidden/Forbidden.tsx
deleted file mode 100644
index dfbe9c34..00000000
--- a/frontend/src/pages/forbidden/Forbidden.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-import BaseErrorPage from '../../components/BaseErrorPage';
-import KirstyImage from '../../images/kirsty.png';
-
-const Forbidden: React.FC = () => {
- return (
-
- );
-};
-
-export default Forbidden;
diff --git a/frontend/src/pages/internalServerError/InternalServerError.tsx b/frontend/src/pages/internalServerError/InternalServerError.tsx
deleted file mode 100644
index 37735d57..00000000
--- a/frontend/src/pages/internalServerError/InternalServerError.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-import BaseErrorPage from '../../components/BaseErrorPage';
-import DeeImage from '../../images/dee.png';
-
-const InternalServerError: React.FC = () => {
- return (
-
- );
-};
-
-export default InternalServerError;
diff --git a/frontend/src/pages/pageNotFound/PageNotFound.tsx b/frontend/src/pages/pageNotFound/PageNotFound.tsx
deleted file mode 100644
index a93c87b7..00000000
--- a/frontend/src/pages/pageNotFound/PageNotFound.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-import BaseErrorPage from '../../components/BaseErrorPage';
-import NigelImage from '../../images/nigel.png';
-
-const PageNotFound: React.FC = () => {
- return (
-
- );
-};
-
-export default PageNotFound;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index e28a0364..ae73ba5a 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -4093,9 +4093,9 @@ coa@^2.0.2:
chalk "^2.4.1"
q "^1.1.2"
-"codeforlife@github:ocadotechnology/codeforlife-package-javascript#v1.15.2":
- version "1.15.2"
- resolved "https://codeload.github.com/ocadotechnology/codeforlife-package-javascript/tar.gz/4e27f768943238910c925d0da9e16c0b19b25556"
+"codeforlife@github:ocadotechnology/codeforlife-package-javascript#v1.15.3":
+ version "1.15.3"
+ resolved "https://codeload.github.com/ocadotechnology/codeforlife-package-javascript/tar.gz/638976530e7fde7bf75fd78efb5fbfabcf7e2a3e"
dependencies:
"@emotion/react" "^11.10.6"
"@emotion/styled" "^11.10.6"