diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aafeb4ab4..2ff35f1011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - ♿️(frontend) restore skip to content link after header redesign #2510 +- 💄(frontend) redesign error pages with Figma layout #2528 ## [v5.4.1] - 2026-07-09 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts index 1dda69e556..758e63a553 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts @@ -141,15 +141,17 @@ test.describe('Doc Routing', () => { await responsePromise; - await expect(page.getByText('Log in to access the document.')).toBeVisible({ - timeout: 10000, - }); + await expect(page.getByText('Sign in to access the document.')).toBeVisible( + { + timeout: 10000, + }, + ); await expect(page.locator('meta[name="robots"]')).toHaveAttribute( 'content', 'noindex', ); - await expect(page).toHaveTitle(/401 Unauthorized - Docs/); + await expect(page).toHaveTitle(/Access denied - Docs/); }); test('checks redirect if unsync version', async ({ page }) => { @@ -202,7 +204,7 @@ test.describe('Doc Routing: Not logged', () => { await expect( page2 .getByRole('main', { name: 'Main content' }) - .getByRole('button', { name: 'Login' }), + .getByRole('button', { name: 'Sign in' }), ).toBeVisible({ timeout: 10000, }); @@ -212,7 +214,7 @@ test.describe('Doc Routing: Not logged', () => { await expect( page .getByRole('main', { name: 'Main content' }) - .getByRole('button', { name: 'Login' }), + .getByRole('button', { name: 'Sign in' }), ).toBeVisible({ timeout: 10000, }); @@ -220,7 +222,7 @@ test.describe('Doc Routing: Not logged', () => { // Reconnected await page .getByRole('main', { name: 'Main content' }) - .getByRole('button', { name: 'Login' }) + .getByRole('button', { name: 'Sign in' }) .click(); await SignIn(page, browserName, false); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index 5488806002..336127a7ee 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -87,7 +87,7 @@ test.describe('Doc Visibility: Restricted', () => { await page.goto(urlDoc); await expect( - page.getByText('Log in to access the document.'), + page.getByText('Sign in to access the document.'), ).toBeVisible(); }); @@ -354,7 +354,7 @@ test.describe('Doc Visibility: Authenticated', () => { await expect(otherPage.locator('h2').getByText(docTitle)).toBeHidden(); await expect( - otherPage.getByText('Log in to access the document.'), + otherPage.getByText('Sign in to access the document.'), ).toBeVisible(); await cleanup(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts index 69dc4c515b..6f8c07a3d2 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts @@ -129,7 +129,7 @@ export const connectOtherUserToDoc = async ({ if (!withoutSignIn) { const loginFromApp = otherPage .getByRole('main', { name: 'Main content' }) - .getByLabel('Login'); + .getByLabel('Sign in'); const loginFromHome = otherPage.getByRole('button', { name: process.env.SIGN_IN_EL_TRIGGER, }); diff --git a/src/frontend/apps/impress/src/assets/icons/404.svg b/src/frontend/apps/impress/src/assets/icons/404.svg new file mode 100644 index 0000000000..d923a0e05b --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/404.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/500.svg b/src/frontend/apps/impress/src/assets/icons/500.svg new file mode 100644 index 0000000000..aee910b78b --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/500.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/503.svg b/src/frontend/apps/impress/src/assets/icons/503.svg new file mode 100644 index 0000000000..1096259d36 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/503.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/DOCS.svg b/src/frontend/apps/impress/src/assets/icons/DOCS.svg new file mode 100644 index 0000000000..50b68e8b6f --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/DOCS.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/Docs Locked.svg b/src/frontend/apps/impress/src/assets/icons/Docs Locked.svg new file mode 100644 index 0000000000..e910ada1d2 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/Docs Locked.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/Mail.svg b/src/frontend/apps/impress/src/assets/icons/Mail.svg new file mode 100644 index 0000000000..93c020a122 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/Mail.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/bubble-text.svg b/src/frontend/apps/impress/src/assets/icons/bubble-text.svg new file mode 100644 index 0000000000..6b59071544 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/bubble-text.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/frontend/apps/impress/src/assets/icons/retry.svg b/src/frontend/apps/impress/src/assets/icons/retry.svg new file mode 100644 index 0000000000..e866c7ad89 --- /dev/null +++ b/src/frontend/apps/impress/src/assets/icons/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/impress/src/components/ErrorPage.tsx b/src/frontend/apps/impress/src/components/ErrorPage.tsx index baf380345b..2e2191ef68 100644 --- a/src/frontend/apps/impress/src/components/ErrorPage.tsx +++ b/src/frontend/apps/impress/src/components/ErrorPage.tsx @@ -1,20 +1,66 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; import Head from 'next/head'; -import Image, { StaticImageData } from 'next/image'; +import Link from 'next/link'; +import { ComponentType, ReactNode, SVGProps } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { Box, Icon, StyledLink, Text } from '@/components'; +import { Box, Icon, Text } from '@/components'; +import HomeSvg from '@/icons/house-rounded.svg'; -const StyledButton = styled(Button)` - width: fit-content; +export const errorActionStyles = ` + display: flex; + height: var(--md, 24px); + padding: 0 var(--xxxs, 4px); + justify-content: center; + align-items: center; + gap: var(--xxxs, 4px); + color: var(--c--contextuals--content--semantic--neutral--tertiary); + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: 12px; + font-weight: 500; + line-height: var(--line-height-xs, 16px); + text-decoration: none; + + svg { + text-decoration: none; + } + + .--docs--error-action-label { + text-decoration: none; + } + + &:hover:not(:disabled) .--docs--error-action-label { + text-decoration: underline; + } +`; + +export const errorDescriptionStyles = ` + color: var(--c--contextuals--content--semantic--neutral--secondary); + font-size: 12px; + font-weight: 400; + line-height: var(--line-height-xs, 16px); + margin-top: 4px; +`; + +export const ErrorActionLink = styled.button` + ${errorActionStyles} +`; + +export const ErrorActionLinkStyled = styled(Link)` + ${errorActionStyles} `; interface ErrorPageProps { - image: StaticImageData; + icon: ComponentType>; + title: string; description: string; refreshTarget?: string; showReload?: boolean; + showHome?: boolean; + actions?: ReactNode; } const getSafeRefreshUrl = (target?: string): string | undefined => { @@ -40,94 +86,103 @@ const getSafeRefreshUrl = (target?: string): string | undefined => { }; export const ErrorPage = ({ - image, + icon: IconComponent, + title, description, refreshTarget, showReload, + showHome = true, + actions, }: ErrorPageProps) => { const { t } = useTranslation(); - const errorTitle = t('An unexpected error occurred.'); const safeTarget = getSafeRefreshUrl(refreshTarget); return ( <> - {errorTitle} - {t('Docs')} + {title} - {t('Docs')} - {errorTitle} - {t('Docs')} + {title} - {t('Docs')} - + ); diff --git a/src/frontend/apps/impress/src/components/ErrorPageFooter.tsx b/src/frontend/apps/impress/src/components/ErrorPageFooter.tsx new file mode 100644 index 0000000000..b7f5d88a71 --- /dev/null +++ b/src/frontend/apps/impress/src/components/ErrorPageFooter.tsx @@ -0,0 +1,59 @@ +import { UserMenu } from '@gouvfr-lasuite/ui-kit'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; +import { Waffle } from '@/components/Waffle'; +import { useAuth } from '@/features/auth/hooks/useAuth'; +import { gotoLogout } from '@/features/auth/utils'; +import { HelpMenu } from '@/features/help'; + +const footerButtonStyles = css` + display: flex; + height: var(--Height, 32px); + min-width: var(--Height, 32px); + padding: 4px; + justify-content: center; + align-items: center; + gap: 7px; + border-radius: 4px; + color: var(--c--contextuals--content--semantic--neutral--tertiary); +`; + +export const ErrorPageFooter = () => { + const { t } = useTranslation(); + const { user } = useAuth(); + + const userMenu = user || { + full_name: t('Guest'), + email: '', + }; + + return ( + + {user && ( + + )} + + + + ); +}; diff --git a/src/frontend/apps/impress/src/components/ErrorPageHeader.tsx b/src/frontend/apps/impress/src/components/ErrorPageHeader.tsx new file mode 100644 index 0000000000..9d0ba5bbfd --- /dev/null +++ b/src/frontend/apps/impress/src/components/ErrorPageHeader.tsx @@ -0,0 +1,64 @@ +import Image from 'next/image'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import DocsTitleSvg from '@/assets/icons/DOCS.svg'; +import { Box, StyledLink } from '@/components'; +import { useConfig } from '@/core'; + +export const ErrorPageHeader = () => { + const { t } = useTranslation(); + const { data: config } = useConfig(); + const icon = config?.theme_customization?.header?.icon; + + return ( + + + + {icon && ( + + )} + + + + ); +}; diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts index 9075311abf..c555974103 100644 --- a/src/frontend/apps/impress/src/components/index.ts +++ b/src/frontend/apps/impress/src/components/index.ts @@ -5,6 +5,8 @@ export * from './DropButton'; export * from './dropdown-menu/DropdownMenu'; export * from './Emoji/EmojiPicker'; export * from './ErrorPage'; +export * from './ErrorPageFooter'; +export * from './ErrorPageHeader'; export * from './quick-search'; export * from './Icon'; export * from './InfiniteScroll'; diff --git a/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx b/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx index 5147b00a70..330a06d99f 100644 --- a/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx +++ b/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx @@ -1,11 +1,17 @@ -import Image from 'next/image'; import { useTranslation } from 'react-i18next'; -import error_img from '@/assets/icons/error-coffee.png'; -import { Box, Loading, Text } from '@/components'; +import Error500Svg from '@/assets/icons/500.svg'; +import EmailConfirmationSvg from '@/assets/icons/Mail.svg'; +import { + Box, + ErrorActionLink, + Icon, + Loading, + Text, + errorDescriptionStyles, +} from '@/components'; import { useUserReconciliationsQuery } from '../api'; -import SuccessSvg from '../assets/mail-check-filled.svg'; interface UserReconciliationProps { reconciliationId: string; @@ -34,54 +40,54 @@ export const UserReconciliation = ({ } let render = ( - - - - {t('Email Address Confirmed')} + + ); if (isError) { render = ( - - + +