🥅(frontend) add boundary error page

Add a custom error page to handle unexpected errors
gracefully. This page provides users with options
to navigate back to the home page or refresh
the current page, enhancing the overall user
experience during error scenarios.
It is quite hard to test this page, it cannot
be trigger in development mode, we have to build
the app and have a real error in production to
see it.
This commit is contained in:
Anthony LC
2025-12-15 17:13:49 +01:00
parent 48aa4971ec
commit 344e9a83e4
7 changed files with 163 additions and 20 deletions

View File

@@ -12,6 +12,7 @@ and this project adheres to
- ✨(backend) add async indexation of documents on save (or access save) #1276
- ✨(backend) add debounce mechanism to limit indexation jobs #1276
- ✨(api) add API route to search for indexed documents in Find #1276
- 🥅(frontend) add boundary error page #1728
### Changed

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -4,6 +4,8 @@ import { CSSProperties, RuleSet } from 'styled-components/dist/types';
import {
MarginPadding,
Spacings,
spacingValue,
stylesMargin,
stylesPadding,
} from '@/utils/styleBuilder';
@@ -22,7 +24,7 @@ export interface BoxProps {
$display?: CSSProperties['display'];
$effect?: 'show' | 'hide';
$flex?: CSSProperties['flex'];
$gap?: CSSProperties['gap'];
$gap?: Spacings;
$hasTransition?: boolean | 'slow';
$height?: CSSProperties['height'];
$justify?: CSSProperties['justifyContent'];
@@ -70,7 +72,7 @@ export const Box = styled('div')<BoxProps>`
${({ $display, as }) =>
`display: ${$display || (as?.match('span|input') ? 'inline-flex' : 'flex')};`}
${({ $flex }) => $flex && `flex: ${$flex};`}
${({ $gap }) => $gap && `gap: ${$gap};`}
${({ $gap }) => $gap && `gap: ${spacingValue($gap)};`}
${({ $height }) => $height && `height: ${$height};`}
${({ $hasTransition }) =>
$hasTransition && $hasTransition === 'slow'

View File

@@ -2,6 +2,7 @@ import { useRouter } from 'next/router';
import { css } from 'styled-components';
import { Box, SeparatedSection } from '@/components';
import { useDocStore } from '@/docs/doc-management';
import { LeftPanelTargetFilters } from './LefPanelTargetFilters';
import { LeftPanelDocContent } from './LeftPanelDocContent';
@@ -11,6 +12,7 @@ export const LeftPanelContent = () => {
const router = useRouter();
const isHome = router.pathname === '/';
const isDoc = router.pathname === '/docs/[id]';
const { currentDoc } = useDocStore();
return (
<>
@@ -36,7 +38,7 @@ export const LeftPanelContent = () => {
</Box>
</>
)}
{isDoc && <LeftPanelDocContent />}
{isDoc && currentDoc && <LeftPanelDocContent doc={currentDoc} />}
</>
);
};

View File

@@ -1,15 +1,13 @@
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { Box } from '@/components';
import { Doc, useDocStore } from '@/docs/doc-management';
import { Doc } from '@/docs/doc-management';
import { DocTree } from '@/docs/doc-tree/';
export const LeftPanelDocContent = () => {
const { currentDoc } = useDocStore();
export const LeftPanelDocContent = ({ doc }: { doc: Doc }) => {
const tree = useTreeContext<Doc>();
if (!currentDoc || !tree) {
if (!tree) {
return null;
}
@@ -20,7 +18,7 @@ export const LeftPanelDocContent = () => {
$css="width: 100%; overflow-y: auto; overflow-x: hidden;"
className="--docs--left-panel-doc-content"
>
<DocTree currentDoc={currentDoc} />
<DocTree currentDoc={doc} />
</Box>
);
};

View File

@@ -5,7 +5,7 @@ import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import img403 from '@/assets/icons/icon-403.png';
import error_img from '@/assets/icons/error-planetes.png';
import { Box, Icon, StyledLink, Text } from '@/components';
import { PageLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -32,32 +32,46 @@ const Page: NextPageWithLayout = () => {
<Box
$align="center"
$margin="auto"
$gap="1rem"
$gap="md"
$padding={{ bottom: '2rem' }}
>
<Text as="h1" $textAlign="center" className="sr-only">
{t('Page Not Found - Error 404')} - {t('Docs')}
</Text>
<Image
src={img403}
src={error_img}
alt=""
width={300}
height={300}
style={{
maxWidth: '100%',
height: 'auto',
}}
/>
<Box $align="center" $gap="0.8rem">
<Text as="p" $textAlign="center" $maxWidth="350px" $theme="brand">
{t(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',
)}
</Text>
<Text
as="p"
$textAlign="center"
$maxWidth="350px"
$theme="neutral"
$margin="0"
>
{t(
'It seems that the page you are looking for does not exist or cannot be displayed correctly.',
)}
</Text>
<Box $direction="row" $gap="sm">
<StyledLink href="/">
<StyledButton icon={<Icon iconName="house" $color="white" />}>
<StyledButton
color="neutral"
icon={
<Icon
iconName="house"
variant="symbols-outlined"
$withThemeInherited
/>
}
>
{t('Home')}
</StyledButton>
</StyledLink>

View File

@@ -0,0 +1,126 @@
import { Button } from '@openfun/cunningham-react';
import * as Sentry from '@sentry/nextjs';
import { NextPageContext } from 'next';
import NextError from 'next/error';
import Head from 'next/head';
import Image from 'next/image';
import { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import error_img from '@/assets/icons/error-planetes.png';
import { Box, Icon, StyledLink, Text } from '@/components';
import { PageLayout } from '@/layouts';
const StyledButton = styled(Button)`
width: fit-content;
`;
const Error = () => {
const { t } = useTranslation();
const errorTitle = t('An unexpected error occurred.');
return (
<>
<Head>
<title>
{errorTitle} - {t('Docs')}
</title>
<meta
property="og:title"
content={`${errorTitle} - ${t('Docs')}`}
key="title"
/>
</Head>
<Box
$align="center"
$margin="auto"
$gap="md"
$padding={{ bottom: '2rem' }}
>
<Text as="h2" $textAlign="center" className="sr-only">
{errorTitle} - {t('Docs')}
</Text>
<Image
src={error_img}
alt=""
width={300}
style={{
maxWidth: '100%',
height: 'auto',
}}
/>
<Text
as="p"
$textAlign="center"
$maxWidth="350px"
$theme="neutral"
$margin="0"
>
{errorTitle}
</Text>
<Box $direction="row" $gap="sm">
<StyledLink href="/">
<StyledButton
color="neutral"
icon={
<Icon
iconName="house"
variant="symbols-outlined"
$withThemeInherited
/>
}
>
{t('Home')}
</StyledButton>
</StyledLink>
<StyledButton
color="neutral"
variant="bordered"
icon={
<Icon
iconName="refresh"
variant="symbols-outlined"
$withThemeInherited
/>
}
onClick={() => window.location.reload()}
>
{t('Refresh page')}
</StyledButton>
</Box>
</Box>
</>
);
};
Error.getInitialProps = async (contextData: NextPageContext) => {
const { res, err, asPath, pathname, query } = contextData;
Sentry.captureException(err, {
contexts: {
nextjs: {
page: pathname,
path: asPath,
query: query,
statusCode: res?.statusCode || err?.statusCode,
},
},
tags: {
errorPage: '_error.tsx',
statusCode: String(res?.statusCode || err?.statusCode || 'unknown'),
},
});
return NextError.getInitialProps(contextData);
};
Error.getLayout = function getLayout(page: ReactElement) {
return <PageLayout withFooter={false}>{page}</PageLayout>;
};
export default Error;