diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index 8b34a81b..dd2f81ab 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -40,6 +40,7 @@ module.exports = {
"jsdoc/require-param": 0,
"jsdoc/require-param-description": 1,
"jsdoc/require-param-name": 1,
+ "jsdoc/require-param-type": 0,
"jsdoc/require-property": 1,
"jsdoc/require-property-description": 1,
"jsdoc/require-property-name": 1,
diff --git a/frontend/cypress/e2e/ErrorPage.cy.tsx b/frontend/cypress/e2e/ErrorPage.cy.tsx
new file mode 100644
index 00000000..7d998994
--- /dev/null
+++ b/frontend/cypress/e2e/ErrorPage.cy.tsx
@@ -0,0 +1,16 @@
+describe('Error page test', () => {
+ it('Error page should load appropriately', () => {
+ expect(
+ () => {
+ cy.request({
+ method: 'POST',
+ path: '**',
+ body: {name: "fail"},
+ failOnStatusCode: false
+ }).then(response => {
+ expect(response.status).to.be(404) // is supposed to be 404
+ })
+ }
+ )
+ })
+})
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7c101d25..4d9a738d 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -30,12 +30,15 @@
"styled-components": "^6.1.8"
},
"devDependencies": {
+ "@types/history": "^4.7.11",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
+ "@types/react-router-dom": "^5.3.3",
+ "@types/scheduler": "^0.23.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
- "cypress": "^13.6.4",
+ "cypress": "^13.7.0",
"eslint": "^8.56.0",
"eslint-plugin-jsdoc": "^48.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -45,6 +48,7 @@
"i18next-browser-languagedetector": "^7.2.1",
"i18next-http-backend": "^2.5.0",
"react-i18next": "^14.1.0",
+ "scheduler": "^0.23.0",
"typescript": "^5.2.2",
"vite": "^5.1.7"
}
@@ -1806,6 +1810,12 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/history": {
+ "version": "4.7.11",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
+ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1826,9 +1836,9 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
- "version": "20.12.6",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.6.tgz",
- "integrity": "sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==",
+ "version": "20.12.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
+ "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
"dev": true,
"optional": true,
"dependencies": {
@@ -1863,6 +1873,27 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-router": {
+ "version": "5.1.20",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
+ "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-router-dom": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
+ "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
@@ -1871,6 +1902,12 @@
"@types/react": "*"
}
},
+ "node_modules/@types/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==",
+ "dev": true
+ },
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
@@ -6368,7 +6405,7 @@
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
diff --git a/frontend/package.json b/frontend/package.json
index 89c7e34f..7ea862ec 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -36,10 +36,13 @@
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
+ "@types/react-router-dom": "^5.3.3",
+ "@types/history": "^4.7.11",
+ "@types/scheduler": "^0.23.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
- "cypress": "^13.6.4",
+ "cypress": "^13.7.0",
"eslint": "^8.56.0",
"eslint-plugin-jsdoc": "^48.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -49,6 +52,7 @@
"i18next-browser-languagedetector": "^7.2.1",
"i18next-http-backend": "^2.5.0",
"react-i18next": "^14.1.0",
+ "scheduler": "^0.23.0",
"typescript": "^5.2.2",
"vite": "^5.1.7"
}
diff --git a/frontend/public/img/error_pigeon.png b/frontend/public/img/error_pigeon.png
new file mode 100644
index 00000000..f2264ead
Binary files /dev/null and b/frontend/public/img/error_pigeon.png differ
diff --git a/frontend/public/logo_ugent.png b/frontend/public/img/logo_ugent.png
similarity index 100%
rename from frontend/public/logo_ugent.png
rename to frontend/public/img/logo_ugent.png
diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json
index 56405918..b9cbb5e1 100644
--- a/frontend/public/locales/en/translation.json
+++ b/frontend/public/locales/en/translation.json
@@ -1,7 +1,4 @@
{
- "home": {
- "title": "Homepage"
- },
"header": {
"myProjects": "My Projects",
"myCourses": "My Courses",
@@ -45,6 +42,16 @@
"minutesAgo": "minutes ago",
"justNow": "just now"
},
+ "error": {
+ "pageNotFound": "Page Not Found",
+ "pageNotFoundMessage": "The requested page was not found.",
+ "forbidden": "Forbidden",
+ "forbiddenMessage": "You don't have access to this resource.",
+ "clientError": "Client Error",
+ "clientErrorMessage": "A client error has occured.",
+ "serverError": "Server Error",
+ "serverErrorMessage": "A server error has occured."
+ },
"projectForm": {
"projectTitle": "Title",
"projectDescription": "Project description",
diff --git a/frontend/public/locales/nl/translation.json b/frontend/public/locales/nl/translation.json
index 6e9312d2..92be172f 100644
--- a/frontend/public/locales/nl/translation.json
+++ b/frontend/public/locales/nl/translation.json
@@ -62,5 +62,15 @@
"hoursAgo": "uur geleden",
"minutesAgo": "minuten geleden",
"justNow": "Zonet"
+ },
+ "error": {
+ "pageNotFound": "Pagina Niet Gevonden",
+ "pageNotFoundMessage": "De opgevraagde pagina werd niet gevonden.",
+ "forbidden": "Verboden",
+ "forbiddenMessage": "Je hebt geen toegang tot deze bron.",
+ "clientError": "Client Fout",
+ "clientErrorMessage": "Er is een client fout opgetreden.",
+ "serverError": "Server Fout",
+ "serverErrorMessage": "Er is een server fout opgetreden."
}
}
\ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 29035b91..881a3dff 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,11 +3,12 @@ import Layout from "./components/Header/Layout";
import Home from "./pages/home/Home";
import LanguagePath from "./components/LanguagePath";
import ProjectView from "./pages/project/projectView/ProjectView";
+import { ErrorBoundary } from "./pages/error/ErrorBoundary.tsx";
import ProjectCreateHome from "./pages/create_project/ProjectCreateHome.tsx";
const router = createBrowserRouter(
createRoutesFromElements(
- }>
+ } errorElement={}>
} />
}>
} />
@@ -27,8 +28,5 @@ const router = createBrowserRouter(
* @returns - The main application component
*/
export default function App(): React.JSX.Element {
- return (
-
-
- );
-}
\ No newline at end of file
+ return ;
+}
diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx
new file mode 100644
index 00000000..2184d5c7
--- /dev/null
+++ b/frontend/src/Layout.tsx
@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+import { Header } from "./components/Header/Header.tsx";
+
+/**
+ * Basic layout component that will be used on all routes.
+ * @returns The Layout component
+ */
+export function Layout(): JSX.Element {
+ return (
+ <>
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/error/ErrorBoundary.tsx b/frontend/src/pages/error/ErrorBoundary.tsx
new file mode 100644
index 00000000..d6d48ca9
--- /dev/null
+++ b/frontend/src/pages/error/ErrorBoundary.tsx
@@ -0,0 +1,32 @@
+import { useRouteError, isRouteErrorResponse } from "react-router-dom";
+import { ErrorPage } from "./ErrorPage.tsx";
+import { useTranslation } from "react-i18next";
+
+/**
+ * This component will render the ErrorPage component with the appropriate data when an error occurs.
+ * @returns The ErrorBoundary component
+ */
+export function ErrorBoundary() {
+ const error = useRouteError();
+ const { t } = useTranslation('translation', { keyPrefix: 'error' });
+
+ if (isRouteErrorResponse(error)) {
+ if (error.status == 404) {
+ return (
+
+ );
+ } else if (error.status == 403) {
+ return (
+
+ );
+ } else if (error.status >= 400 && error.status <= 499) {
+ return (
+
+ );
+ } else if (error.status >= 500 && error.status <= 599) {
+ return (
+
+ );
+ }
+ }
+}
diff --git a/frontend/src/pages/error/ErrorPage.tsx b/frontend/src/pages/error/ErrorPage.tsx
new file mode 100644
index 00000000..edabd86e
--- /dev/null
+++ b/frontend/src/pages/error/ErrorPage.tsx
@@ -0,0 +1,51 @@
+import { Grid, Typography } from "@mui/material";
+
+/**
+ * This component will be rendered when an error occurs.
+ * @param statusCode - The status code of the error
+ * @param statusTitle - The name of the error
+ * @param message - Additional information about the error
+ * @returns The ErrorPage component
+ */
+export function ErrorPage(
+ { statusCode, statusTitle, message }: { statusCode: string, statusTitle: string, message: string }
+): React.JSX.Element {
+ return (
+
+
+
+
+
+ { statusCode }
+
+
+
+
+
+
+
+
+
+ { statusTitle }
+
+
+
+
+ { message }
+
+
+
+ );
+}