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 } + + + + icon + + + + + + { statusTitle } + + + + + { message } + + + + ); +}