diff --git a/src/apps/platform/src/platform.routes.tsx b/src/apps/platform/src/platform.routes.tsx index a39f1f82e..5a8f9d691 100644 --- a/src/apps/platform/src/platform.routes.tsx +++ b/src/apps/platform/src/platform.routes.tsx @@ -25,6 +25,11 @@ const Home: LazyLoadedComponent = lazyLoad( 'HomePage', ) +const NotFound: LazyLoadedComponent = lazyLoad( + () => import('./routes/not-found'), + 'NotFoundPage', +) + const homeRoutes: ReadonlyArray = [ { element: , @@ -33,6 +38,17 @@ const homeRoutes: ReadonlyArray = [ }, ] +// Catch-all for paths Platform UI is served for but does not own a route for, +// eg. `/opportunities` on the Topcoder apex host. React-router ranks the `*` +// path last, so this never shadows a route declared above. +const notFoundRoutes: ReadonlyArray = [ + { + element: , + id: 'Not found page', + route: '*', + }, +] + export const platformRoutes: Array = [ // NOTE: Order matters here bc the active tool // is determined by finding the first route @@ -57,4 +73,5 @@ export const platformRoutes: Array = [ ...adminRoutes, ...reportsRoutes, ...customerPortalRoutes, + ...notFoundRoutes, ] diff --git a/src/apps/platform/src/routes/not-found/NotFound.module.scss b/src/apps/platform/src/routes/not-found/NotFound.module.scss new file mode 100644 index 000000000..6fab0c29a --- /dev/null +++ b/src/apps/platform/src/routes/not-found/NotFound.module.scss @@ -0,0 +1,31 @@ +@import "@libs/ui/styles/includes"; + +.container { + min-height: 60vh; + display: flex; + align-items: center; + justify-content: center; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: $sp-6; + padding: $sp-15 $sp-8; + color: $black-100; + + svg { + color: $black-60; + } +} + +.title { + @include font-barlow; + font-weight: 600; + font-size: 28px; + line-height: 34px; + margin: 0; +} diff --git a/src/apps/platform/src/routes/not-found/NotFound.spec.tsx b/src/apps/platform/src/routes/not-found/NotFound.spec.tsx new file mode 100644 index 000000000..f2cb983d1 --- /dev/null +++ b/src/apps/platform/src/routes/not-found/NotFound.spec.tsx @@ -0,0 +1,53 @@ +/* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports */ +import '@testing-library/jest-dom' +import type { PropsWithChildren } from 'react' +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' + +import NotFoundPage from './NotFound' + +jest.mock('~/libs/ui', () => ({ + ContentLayout: (props: PropsWithChildren): JSX.Element =>
{props.children}
, + IconOutline: { + ExclamationCircleIcon: (): JSX.Element => , + }, + LinkButton: (props: PropsWithChildren<{ to: string }>): JSX.Element => ( + {props.children} + ), + PageTitle: (): JSX.Element => <>, +}), { virtual: true }) + +// `getRouteElement` renders a childless `PlatformRoute` as ``, +// so these paths mirror the platform `homeRoutes` and `notFoundRoutes` entries. +function renderPlatformRoutes(pathname: string): void { + render( + + + home page} path='' /> + } path='*' /> + + , + ) +} + +describe('NotFoundPage', () => { + it('renders a message instead of a blank page for an unmatched path', () => { + renderPlatformRoutes('/opportunities') + + expect(screen.getByRole('alert')) + .toBeInTheDocument() + expect(screen.getByText('We were unable to find that page')) + .toBeInTheDocument() + expect(screen.getByText('Go to the home page')) + .toHaveAttribute('href', '/') + }) + + it('does not shadow a route the platform does own', () => { + renderPlatformRoutes('/') + + expect(screen.getByText('home page')) + .toBeInTheDocument() + expect(screen.queryByRole('alert')) + .not.toBeInTheDocument() + }) +}) diff --git a/src/apps/platform/src/routes/not-found/NotFound.tsx b/src/apps/platform/src/routes/not-found/NotFound.tsx new file mode 100644 index 000000000..790e6bd29 --- /dev/null +++ b/src/apps/platform/src/routes/not-found/NotFound.tsx @@ -0,0 +1,35 @@ +import { FC } from 'react' + +import { + ContentLayout, + IconOutline, + LinkButton, + PageTitle, +} from '~/libs/ui' + +import styles from './NotFound.module.scss' + +/** + * Fallback page for a path Platform UI is served for but has no route for. + * + * Without it react-router matches nothing, the router renders an empty + * container inside the app shell, and the user is shown a blank page. + */ +const NotFoundPage: FC<{}> = () => ( + + Page Not Found | Topcoder + +
+ +

We were unable to find that page

+

+ The page you requested does not exist or is not available yet. +

+ + Go to the home page + +
+
+) + +export default NotFoundPage diff --git a/src/apps/platform/src/routes/not-found/index.ts b/src/apps/platform/src/routes/not-found/index.ts new file mode 100644 index 000000000..6668cd4c6 --- /dev/null +++ b/src/apps/platform/src/routes/not-found/index.ts @@ -0,0 +1 @@ +export { default as NotFoundPage } from './NotFound'