Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ErrorPageTemplateProps = {
const ErrorPageTemplate: React.FC<ErrorPageTemplateProps> = ({ children }) => {
return (
<Container maxWidth="sm">
<MuiCard sx={{ marginTop: "1rem" }}>
<MuiCard sx={{ marginTop: "4rem" }}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were colliding:

image

After:
image

{/* TODO <Helmet>
<title>{`${title} | ${APP_SETTINGS.SITE_NAME}`}</title>
<meta name="robots" content="noindex,noarchive" />
Expand Down
18 changes: 18 additions & 0 deletions frontends/main/src/app-pages/ErrorPage/GlobalErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client"

import React from "react"
import ErrorPageTemplate from "./ErrorPageTemplate"
import { Typography } from "ol-components"

const GlobalErrorPage = ({ error }: { error: Pick<Error, "message"> }) => {
return (
<ErrorPageTemplate title="Unexpected Error">
<Typography variant="h3" component="h1">
Unexpected Error
</Typography>
{error.message || ""}
</ErrorPageTemplate>
)
}

export default GlobalErrorPage
12 changes: 7 additions & 5 deletions frontends/main/src/app/c/[channelType]/[name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ChannelPage from "@/app-pages/ChannelPage/ChannelPage"
import { channelsApi } from "api/clients"
import { ChannelTypeEnum } from "api/v0"
import { getMetadataAsync } from "@/common/metadata"
import handleNotFound from "@/common/handleNotFound"

type RouteParams = {
channelType: ChannelTypeEnum
Expand All @@ -18,13 +19,14 @@ export async function generateMetadata({
}) {
const { channelType, name } = params

const channelDetails = await channelsApi
.channelsTypeRetrieve({ channel_type: channelType, name: name })
.then((res) => res.data)
const { data } = await handleNotFound(
channelsApi.channelsTypeRetrieve({ channel_type: channelType, name: name }),
)

return getMetadataAsync({
searchParams,
title: `${channelDetails.title}`,
description: channelDetails.public_description,
title: data.title,
description: data.public_description,
})
}

Expand Down
19 changes: 19 additions & 0 deletions frontends/main/src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client"

/* This is the catch-all error page that receives errors from server rendered root layout
* components and metadata.
* It is only enabled in production so that in development we see the Next.js error overlay.
* It is passed an error object as an argument, though this has been stripped of everything except
* the message and a digest for server logs correlation; to prevent leaking anything to the client
*
* https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-errors-in-root-layouts
*/

import React from "react"
import GlobalErrorPage from "@/app-pages/ErrorPage/GlobalErrorPage"

const GlobalError = ({ error }: { error: Error }) => {
return <GlobalErrorPage error={error} />
}

export default GlobalError
39 changes: 39 additions & 0 deletions frontends/main/src/common/handleNotFound.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { AxiosError } from "axios"
import handleNotFound from "./handleNotFound"
import { nextNavigationMocks } from "ol-test-utilities/mocks/nextNavigation"

describe("Handle not found wrapper utility", () => {
test("Should call notFound() for errors with status 404", async () => {
const error: Partial<AxiosError> = {
status: 404,
message: "Not Found",
}

const promise = Promise.reject(error)

await handleNotFound(promise)

expect(nextNavigationMocks.notFound).toHaveBeenCalled()
})

test("Should not call notFound() for success and return result", async () => {
const resolvedValue = { data: "success" }
const promise = Promise.resolve(resolvedValue)

const result = await handleNotFound(promise)

expect(result).toEqual(resolvedValue)
expect(nextNavigationMocks.notFound).not.toHaveBeenCalled()
})

test("Should rethrow non 404 errors", async () => {
const error = new Error("Something went wrong")

const promise = Promise.reject(error)

await expect(handleNotFound(promise)).rejects.toThrow(
"Something went wrong",
)
expect(nextNavigationMocks.notFound).not.toHaveBeenCalled()
})
})
24 changes: 24 additions & 0 deletions frontends/main/src/common/handleNotFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { AxiosError } from "axios"
import { notFound } from "next/navigation"

/* This is intended to wrap API calls that fetch resources during server render,
* such as to gather metadata for the learning resource drawer.
*
* The ./app/global-error.tsx boundary for root layout errors is only supplied the
* error message so we cannot determine that it is a 404 to show the NotFoundPage.
* Instead we must handle at each point of use so need a utility function. Below we
* use next/navigation's notFound() to render ./app/not-found.tsx
*/

const handleNotFound = async <T>(promise: Promise<T | never>): Promise<T> => {
try {
return await promise
} catch (error) {
if ((error as AxiosError).status === 404) {
return notFound()
}
throw error
}
}

export default handleNotFound
22 changes: 9 additions & 13 deletions frontends/main/src/common/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
import { learningResourcesApi } from "api/clients"
import type { Metadata } from "next"
import handleNotFound from "./handleNotFound"

const DEFAULT_OG_IMAGE = "/images/learn-og-image.jpg"

Expand Down Expand Up @@ -29,21 +30,16 @@ export const getMetadataAsync = async ({
// The learning resource drawer is open
const learningResourceId = searchParams?.[RESOURCE_DRAWER_QUERY_PARAM]
if (learningResourceId) {
try {
const { data } = await learningResourcesApi.learningResourcesRetrieve({
const { data } = await handleNotFound(
learningResourcesApi.learningResourcesRetrieve({
id: Number(learningResourceId),
})
}),
)

title = data?.title
description = data?.description?.replace(/<\/[^>]+(>|$)/g, "") ?? ""
image = data?.image?.url || image
imageAlt = image === data?.image?.url ? imageAlt : data?.image?.alt || ""
} catch (error) {
console.warn("Failed to fetch learning resource", {
learningResourceId,
error,
})
}
title = data?.title
description = data?.description?.replace(/<\/[^>]+(>|$)/g, "") ?? ""
image = data?.image?.url || image
imageAlt = image === data?.image?.url ? imageAlt : data?.image?.alt || ""
}

return standardizeMetadata({
Expand Down
Loading