Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/apps/platform/src/platform.routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const Home: LazyLoadedComponent = lazyLoad(
'HomePage',
)

const NotFound: LazyLoadedComponent = lazyLoad(
() => import('./routes/not-found'),
'NotFoundPage',
)

const homeRoutes: ReadonlyArray<PlatformRoute> = [
{
element: <Home />,
Expand All @@ -33,6 +38,17 @@ const homeRoutes: ReadonlyArray<PlatformRoute> = [
},
]

// 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<PlatformRoute> = [
{
element: <NotFound />,
id: 'Not found page',
route: '*',
},
]

export const platformRoutes: Array<PlatformRoute> = [
// NOTE: Order matters here bc the active tool
// is determined by finding the first route
Expand All @@ -57,4 +73,5 @@ export const platformRoutes: Array<PlatformRoute> = [
...adminRoutes,
...reportsRoutes,
...customerPortalRoutes,
...notFoundRoutes,
]
31 changes: 31 additions & 0 deletions src/apps/platform/src/routes/not-found/NotFound.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
53 changes: 53 additions & 0 deletions src/apps/platform/src/routes/not-found/NotFound.spec.tsx
Original file line number Diff line number Diff line change
@@ -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 => <div>{props.children}</div>,
IconOutline: {
ExclamationCircleIcon: (): JSX.Element => <svg />,
},
LinkButton: (props: PropsWithChildren<{ to: string }>): JSX.Element => (
<a href={props.to}>{props.children}</a>
),
PageTitle: (): JSX.Element => <></>,
}), { virtual: true })

// `getRouteElement` renders a childless `PlatformRoute` as `<Route path={route.route} />`,
// so these paths mirror the platform `homeRoutes` and `notFoundRoutes` entries.
function renderPlatformRoutes(pathname: string): void {
render(
<MemoryRouter initialEntries={[pathname]}>
<Routes>
<Route element={<div>home page</div>} path='' />
<Route element={<NotFoundPage />} path='*' />
</Routes>
</MemoryRouter>,
)
}

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()
})
})
35 changes: 35 additions & 0 deletions src/apps/platform/src/routes/not-found/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -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<{}> = () => (
<ContentLayout outerClass={styles.container}>
<PageTitle>Page Not Found | Topcoder</PageTitle>

<div className={styles.content} role='alert'>
<IconOutline.ExclamationCircleIcon className='icon-xxxl' />
<h2 className={styles.title}>We were unable to find that page</h2>
<p className='body-main'>
The page you requested does not exist or is not available yet.
</p>
<LinkButton primary size='lg' to='/'>
Go to the home page
</LinkButton>
</div>
</ContentLayout>
)

export default NotFoundPage
1 change: 1 addition & 0 deletions src/apps/platform/src/routes/not-found/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as NotFoundPage } from './NotFound'
Loading