🥅(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:
@@ -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
|
||||
|
||||
|
||||
BIN
src/frontend/apps/impress/src/assets/icons/error-planetes.png
Normal file
BIN
src/frontend/apps/impress/src/assets/icons/error-planetes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -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'
|
||||
|
||||
@@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
126
src/frontend/apps/impress/src/pages/_error.tsx
Normal file
126
src/frontend/apps/impress/src/pages/_error.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user