diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index dc8fb47ca3..b91f1f1a07 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -34,8 +34,6 @@ jobs: build: needs: deploy-context env: - FR_BASE_URL: ${{ needs.deploy-context.outputs.fr_url }} - EN_BASE_URL: ${{ needs.deploy-context.outputs.en_url }} VITE_FR_BASE_URL: ${{ needs.deploy-context.outputs.fr_url }} VITE_EN_BASE_URL: ${{ needs.deploy-context.outputs.en_url }} runs-on: ubuntu-18.04 @@ -65,8 +63,8 @@ jobs: yarn workspace site build:prerender - name: Replace site placeholders in netlify.toml redirection file run: | - sed -i "s|:SITE_FR|$FR_BASE_URL|g" site/netlify.toml; - sed -i "s|:SITE_EN|$EN_BASE_URL|g" site/netlify.toml; + sed -i "s|:SITE_FR|$VITE_FR_BASE_URL|g" site/netlify.toml; + sed -i "s|:SITE_EN|$VITE_EN_BASE_URL|g" site/netlify.toml; - name: Update Algolia index run: yarn workspace site algolia:update env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22d7d483da..73acd9c8cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,10 +23,10 @@ Nous utilisons : - [React](https://reactjs.org) pour la gestion de l'interface utilisateur - [Redux](https://redux.js.org) pour gérer le “state” de l'application côté client - [Prettier](https://prettier.io/) pour formater le code source, l'idéal est de configurer votre éditeur de texte pour que les fichiers soit formatés automatiquement quand vous sauvegardez un fichier. Si vous utilisez [VS Code](https://code.visualstudio.com/) cette configuration est automatique. -- [Webpack](https://webpack.js.org) pour le “bundling” +- [ViteJS](https://vitejs.dev) pour le “bundling” et le serveur de développement - [Eslint](http://eslint.org) qui permet par exemple d'éviter de garder des variables inutilisées - [Ramda](https://ramdajs.com) comme libraire d'utilitaires pour manipuler les listes/objects/etc (c'est une alternative à lodash ou underscore) -- [Mocha](https://mochajs.org), [Jest](https://jestjs.io) et [Cypress](https://www.cypress.io) pour les l'execution des tests. Plus d'informations dans la section consacrée aux tests. +- [Vitest](https://vitest.dev) et [Cypress](https://www.cypress.io) pour les l'execution des tests. Plus d'informations dans la section consacrée aux tests. ### Démarrage @@ -121,10 +121,10 @@ yarn test #### Tests de non-regression (snapshots) ```sh -yarn test:regressions +yarn test regressions ``` -Si vous souhaitez mettre à jour les snapshots vous pouvez utiliser le paramètre `--updateSnapshot`, son raccourci `-u`, ou encore le [mode interactif](https://jestjs.io/docs/en/snapshot-testing#interactive-snapshot-mode). +Si vous souhaitez mettre à jour les snapshots vous pouvez utiliser le paramètre `--update`, son raccourci `-u`. #### Tests d'integrations diff --git a/site/package.json b/site/package.json index 53e9ca8ec3..cb4bebc0f3 100644 --- a/site/package.json +++ b/site/package.json @@ -61,7 +61,6 @@ "@react-stately/radio": "^3.3.2", "@react-stately/searchfield": "^3.1.3", "@react-stately/toggle": "^3.2.3", - "@rehooks/local-storage": "2.4.0", "@sentry/react": "^6.3.5", "@sentry/tracing": "^6.3.5", "algoliasearch": "^4.10.2", @@ -93,7 +92,6 @@ "regenerator-runtime": "^0.13.3", "reselect": "^4.0.0", "styled-components": "^5.3.1", - "swr": "^0.1.16", "whatwg-fetch": "^3.0.0", "yaml": "^1.9.2" }, diff --git a/site/prerender.cjs b/site/prerender.cjs index 2a5d0e6019..ad39be3615 100644 --- a/site/prerender.cjs +++ b/site/prerender.cjs @@ -45,6 +45,8 @@ const templates = Object.fromEntries( ) })() +const htmlBodyStart = '' +const htmlBodyEnd = '' const headTagsStart = '' const headTagsEnd = '' @@ -53,7 +55,7 @@ async function prerenderUrl(url, site) { // TODO: Add CI test to enforce meta tags on SSR pages const { html, styleTags, helmet } = await render(url, lang) const page = templates[site] - .replace('', html) + .replace(new RegExp(htmlBodyStart + '[\\s\\S]+' + htmlBodyEnd, 'm'), html) .replace('', styleTags) .replace( new RegExp(headTagsStart + '[\\s\\S]+' + headTagsEnd, 'm'), diff --git a/site/source/Provider.tsx b/site/source/Provider.tsx index f83e393738..edccc21457 100644 --- a/site/source/Provider.tsx +++ b/site/source/Provider.tsx @@ -31,7 +31,7 @@ import { // ATInternet Tracking import { TrackingContext } from './ATInternetTracking' import { createTracker } from './ATInternetTracking/Tracker' -import safeLocalStorage from './storage/safeLocalStorage' +import * as safeLocalStorage from './storage/safeLocalStorage' import { inIframe } from './utils' if ( diff --git a/site/source/components/Feedback/index.tsx b/site/source/components/Feedback/index.tsx index 81d89c51af..d91d5d1295 100644 --- a/site/source/components/Feedback/index.tsx +++ b/site/source/components/Feedback/index.tsx @@ -10,7 +10,7 @@ import { Trans } from 'react-i18next' import { useLocation } from 'react-router-dom' import styled from 'styled-components' import { TrackingContext } from '../../ATInternetTracking' -import safeLocalStorage from '../../storage/safeLocalStorage' +import * as safeLocalStorage from '../../storage/safeLocalStorage' import { INSCRIPTION_LINK } from '../layout/Footer/InscriptionBetaTesteur' import './Feedback.css' import Form from './FeedbackForm' diff --git a/site/source/components/layout/Footer/Privacy.tsx b/site/source/components/layout/Footer/Privacy.tsx index 76f44318d8..85d4cbe53e 100644 --- a/site/source/components/layout/Footer/Privacy.tsx +++ b/site/source/components/layout/Footer/Privacy.tsx @@ -5,7 +5,7 @@ import { Body, SmallBody } from 'DesignSystem/typography/paragraphs' import { useCallback, useContext, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { TrackingContext, TrackPage } from '../../../ATInternetTracking' -import safeLocalStorage from '../../../storage/safeLocalStorage' +import * as safeLocalStorage from '../../../storage/safeLocalStorage' export default function Privacy({ label }: { label?: string }) { const tracker = useContext(TrackingContext) diff --git a/site/source/components/layout/Header.tsx b/site/source/components/layout/Header.tsx index 2046f1725b..19c69e60d8 100644 --- a/site/source/components/layout/Header.tsx +++ b/site/source/components/layout/Header.tsx @@ -1,5 +1,6 @@ import { Logo } from 'Components/Logo' import SearchButton from 'Components/SearchButton' +import BrowserOnly from 'Components/utils/BrowserOnly' import { SitePathsContext } from 'Components/utils/SitePathsContext' import { Container } from 'DesignSystem/layout' import { Link } from 'DesignSystem/typography/link' @@ -28,8 +29,9 @@ export default function Header() { /> {language === 'fr' && } - - {!import.meta.env.SSR && } + + + ) } diff --git a/site/source/components/layout/NewsBanner.tsx b/site/source/components/layout/NewsBanner.tsx index 1662d77dcf..a9b5c544d9 100644 --- a/site/source/components/layout/NewsBanner.tsx +++ b/site/source/components/layout/NewsBanner.tsx @@ -1,56 +1,59 @@ -// import { useLocalStorage, writeStorage } from '@rehooks/local-storage' import { Appear } from 'Components/ui/animate' import Emoji from 'Components/utils/Emoji' import { SitePathsContext } from 'Components/utils/SitePathsContext' import lastRelease from 'Data/last-release.json' import { Banner, HideButton, InnerBanner } from 'DesignSystem/banner' import { Link } from 'DesignSystem/typography/link' -import { useContext, useEffect } from 'react' +import { useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { getItem, setItem } from '../../storage/safeLocalStorage' const localStorageKey = 'last-viewed-release' export const hideNewsBanner = () => { - // writeStorage(localStorageKey, lastRelease.name) + setItem(localStorageKey, lastRelease.name) } + export const determinant = (word: string) => /^[aeiouy]/i.exec(word) ? 'd’' : 'de ' export default function NewsBanner() { - // const [lastViewedRelease] = useLocalStorage(localStorageKey) const sitePaths = useContext(SitePathsContext) - const { i18n, t } = useTranslation() + const { t } = useTranslation() + const lastViewedRelease = getItem(localStorageKey) + const [showBanner, setShowBanner] = useState( + lastViewedRelease && lastViewedRelease !== lastRelease.name + ) // We only want to show the banner to returning visitors, so we initiate the // local storage value with the last release. useEffect(() => { - // writeStorage( - // localStorageKey, - // lastViewedRelease === undefined ? lastRelease.name : lastViewedRelease - // ) + setItem( + localStorageKey, + lastViewedRelease == undefined ? lastRelease.name : lastViewedRelease + ) }, []) - const showBanner = false - if (!showBanner) { return null } return ( - - - - - Découvrez les nouveautés{' '} - {determinant(lastRelease.name)} - - {lastRelease.name.toLowerCase()} - - - - × - - - - + setShowBanner(false)}> + + + Découvrez les nouveautés{' '} + {determinant(lastRelease.name)} + + {lastRelease.name.toLowerCase()} + + + setShowBanner(false)} + aria-label={t('Fermer')} + > + × + + + ) } diff --git a/site/source/components/utils/BrowserOnly.tsx b/site/source/components/utils/BrowserOnly.tsx new file mode 100644 index 0000000000..19182b4119 --- /dev/null +++ b/site/source/components/utils/BrowserOnly.tsx @@ -0,0 +1,15 @@ +import { Appear } from 'Components/ui/animate' +import React from 'react' + +// We add a animation for all coponents displayed on the client only but not on +// the SSR to avoid augment the CLS (Cumulative Layout Shift). +export default function BrowserOnly({ + children, +}: { + children: React.ReactNode +}) { + if (import.meta.env.SSR) { + return null + } + return {children} +} diff --git a/site/source/components/utils/persistState.ts b/site/source/components/utils/persistState.ts index d8e174aebb..37b07c0d7f 100644 --- a/site/source/components/utils/persistState.ts +++ b/site/source/components/utils/persistState.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import safeLocalStorage from '../../storage/safeLocalStorage' +import * as safeLocalStorage from '../../storage/safeLocalStorage' export const getInitialState = (key: string) => { const value = safeLocalStorage.getItem(key) diff --git a/site/source/locales/ui-en.yaml b/site/source/locales/ui-en.yaml index d4985f10d2..e940774a56 100644 --- a/site/source/locales/ui-en.yaml +++ b/site/source/locales/ui-en.yaml @@ -1675,17 +1675,10 @@ replier: show less responsabilité: bouton1: Limited liability company bouton2: Sole proprietorship - description: " - - \t\t\t\t\t\tLimited liability? Sole proprietorship? Each option has - - \t\t\t\t\t\tlegal implications and leads to a different status for the - - \t\t\t\t\t\tyour business in France. This guide helps you choose - - \t\t\t\t\t\tbetween the different forms of liability. - - \t\t\t\t\t" + description: Limited liability? Sole proprietorship? Each option has legal + implications and leads to a different status for the creation of your + company in France. This guide helps you choose between the different forms + of liability. entreprise-individuelle: > <0>Sole proprietorship: diff --git a/site/source/locales/ui-fr.yaml b/site/source/locales/ui-fr.yaml index b6409b8e54..ca574f9c59 100644 --- a/site/source/locales/ui-fr.yaml +++ b/site/source/locales/ui-fr.yaml @@ -1313,20 +1313,10 @@ recherche-globale: responsabilité: bouton1: Société bouton2: Entreprise individuelle - description: " - - \t\t\t\t\t\tResponsabilité limitée ? entreprise individuelle ? Chaque - option a - - \t\t\t\t\t\tdes implications juridiques et conduit à un statut différent - pour la - - \t\t\t\t\t\tcréation de votre entreprise en France. Ce guide vous aide à - choisir - - \t\t\t\t\t\tentre les différentes forme de responsabilité. - - \t\t\t\t\t" + description: Responsabilité limitée ? entreprise individuelle ? Chaque option a + des implications juridiques et conduit à un statut différent pour la + création de votre entreprise en France. Ce guide vous aide à choisir entre + les différentes forme de responsabilité. entreprise-individuelle: "<0>Entreprise individuelle : Une activité économique exercée par une seule personne physique, en son nom propre. Moins de formalités, mais plus de risques en cas de faillite, car votre patrimoine diff --git a/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx b/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx index 5ee12618b6..0c6967c2c4 100644 --- a/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx +++ b/site/source/pages/Creer/GuideStatut/SoleProprietorship.tsx @@ -28,12 +28,7 @@ export default function SoleProprietorship() { name="description" content={t( 'responsabilité.description', - ` - Responsabilité limitée ? entreprise individuelle ? Chaque option a - des implications juridiques et conduit à un statut différent pour la - création de votre entreprise en France. Ce guide vous aide à choisir - entre les différentes forme de responsabilité. - ` + 'Responsabilité limitée ? entreprise individuelle ? Chaque option a des implications juridiques et conduit à un statut différent pour la création de votre entreprise en France. Ce guide vous aide à choisir entre les différentes forme de responsabilité.' )} /> diff --git a/site/source/pages/Gerer/DemandeMobilite/index.tsx b/site/source/pages/Gerer/DemandeMobilite/index.tsx index 00b20d00c1..1123770113 100644 --- a/site/source/pages/Gerer/DemandeMobilite/index.tsx +++ b/site/source/pages/Gerer/DemandeMobilite/index.tsx @@ -2,6 +2,7 @@ import { Grid } from '@mui/material' import RuleInput from 'Components/conversation/RuleInput' import { WhenApplicable, WhenNotApplicable } from 'Components/EngineValue' import PageHeader from 'Components/PageHeader' +import BrowserOnly from 'Components/utils/BrowserOnly' import Emoji from 'Components/utils/Emoji' import { EngineContext, EngineProvider } from 'Components/utils/EngineContext' import { Markdown } from 'Components/utils/markdown' @@ -175,11 +176,11 @@ function FormulairePublicodes() { - {!import.meta.env.SSR && ( + - )} + {!!Object.keys(situation).length && ( diff --git a/site/source/pages/Nouveautes/Nouveautes.tsx b/site/source/pages/Nouveautes/Nouveautes.tsx index e7a04471d6..db2cee1aca 100644 --- a/site/source/pages/Nouveautes/Nouveautes.tsx +++ b/site/source/pages/Nouveautes/Nouveautes.tsx @@ -11,13 +11,11 @@ import { Container } from 'DesignSystem/layout' import { H1 } from 'DesignSystem/typography/heading' import { GenericButtonOrLinkProps, Link } from 'DesignSystem/typography/link' import { Body } from 'DesignSystem/typography/paragraphs' -import { useContext, useEffect, useMemo } from 'react' +import { useContext, useEffect, useMemo, useState } from 'react' import { Redirect, useHistory, useRouteMatch } from 'react-router-dom' import styled from 'styled-components' -import useSWR from 'swr' import { TrackPage } from '../../ATInternetTracking' -const fetcher = (url: RequestInfo) => fetch(url).then((r) => r.json()) const slugify = (name: string) => name.toLowerCase().replace(' ', '-') type ReleasesData = Array<{ @@ -27,9 +25,13 @@ type ReleasesData = Array<{ export default function Nouveautés() { // The release.json file may be big, we don't want to include it in the main - // bundle, that's why we only fetch it on this page. Alternatively we could - // use import("data/release.json") and configure code splitting with Webpack. - const { data } = useSWR('/data/releases.json', fetcher) + // bundle, that's why we only fetch it on this page. + const [data, setData] = useState([]) + useEffect(() => { + import('Data/releases.json').then(({ default: data }) => { + setData(data) + }) + }, []) const history = useHistory() const sitePaths = useContext(SitePathsContext) const slug = useRouteMatch<{ slug: string }>(`${sitePaths.nouveautés}/:slug`) @@ -41,7 +43,7 @@ export default function Nouveautés() { [data] ) - if (!data) { + if (data.length === 0) { return null } @@ -55,7 +57,6 @@ export default function Nouveautés() { } const releaseName = data[selectedRelease].name.toLowerCase() - return ( <> diff --git "a/site/source/pages/Simulateurs/Salari\303\251.tsx" "b/site/source/pages/Simulateurs/Salari\303\251.tsx" index 4ae4d4a445..0c36172c35 100644 --- "a/site/source/pages/Simulateurs/Salari\303\251.tsx" +++ "b/site/source/pages/Simulateurs/Salari\303\251.tsx" @@ -20,6 +20,7 @@ import { Trans } from 'react-i18next' import { useSelector } from 'react-redux' import { targetUnitSelector } from 'Selectors/simulationSelectors' import styled from 'styled-components' +import BrowserOnly from 'Components/utils/BrowserOnly' const ButtonContainer = styled.div` margin: 2rem 1rem; @@ -47,19 +48,21 @@ export default function SalariéSimulation() { } > - {/** L'équipe Code Du Travail Numérique ne souhaite pas référencer - * le simulateur dirigeant de SASU sur son site. */} - {!import.meta.env.SSR && - !document.referrer?.includes('code.travail.gouv.fr') && ( - - - Vous êtes dirigeant d'une SAS(U) ?{' '} - - Accéder au simulateur de revenu dédié - - - - )} + + {/** L'équipe Code Du Travail Numérique ne souhaite pas référencer + * le simulateur dirigeant de SASU sur son site. */} + {!import.meta.env.SSR && + !document.referrer?.includes('code.travail.gouv.fr') && ( + + + Vous êtes dirigeant d'une SAS(U) ?{' '} + + Accéder au simulateur de revenu dédié + + + + )} + ) diff --git a/site/source/storage/persistInFranceApp.ts b/site/source/storage/persistInFranceApp.ts index c10ad500b9..4fc97c4597 100644 --- a/site/source/storage/persistInFranceApp.ts +++ b/site/source/storage/persistInFranceApp.ts @@ -3,7 +3,7 @@ import { InFranceAppState } from 'Reducers/inFranceAppReducer' import { RootState } from 'Reducers/rootReducer' import { Store } from 'redux' import { debounce } from '../utils' -import safeLocalStorage from './safeLocalStorage' +import * as safeLocalStorage from './safeLocalStorage' const VERSION = 7 diff --git a/site/source/storage/persistSimulation.ts b/site/source/storage/persistSimulation.ts index 5c2007ff6d..2c0b9bdd0a 100644 --- a/site/source/storage/persistSimulation.ts +++ b/site/source/storage/persistSimulation.ts @@ -3,7 +3,7 @@ import { RootState } from 'Reducers/rootReducer' import { Store } from 'redux' import { PreviousSimulation } from 'Selectors/previousSimulationSelectors' import { debounce } from '../utils' -import safeLocalStorage from './safeLocalStorage' +import * as safeLocalStorage from './safeLocalStorage' import { deserialize, serialize } from './serializeSimulation' const VERSION = 5 diff --git a/site/source/storage/safeLocalStorage.ts b/site/source/storage/safeLocalStorage.ts index 0c9e795c58..43bbcc57a7 100644 --- a/site/source/storage/safeLocalStorage.ts +++ b/site/source/storage/safeLocalStorage.ts @@ -1,41 +1,35 @@ -export default { - removeItem: function (key: string) { - try { - return window.localStorage.removeItem(key) - } catch (error) { - if (error instanceof Error && error.name === 'SecurityError') { - // eslint-disable-next-line no-console - console.warn( - '[localStorage] Unable to remove item due to security settings' - ) - } - return null +export function removeItem(key: string) { + try { + return window.localStorage.removeItem(key) + } catch (error) { + if (error instanceof Error && error.name === 'SecurityError') { + // eslint-disable-next-line no-console + console.warn( + '[localStorage] Unable to remove item due to security settings' + ) } - }, - getItem: function (key: string) { - try { - return window.localStorage.getItem(key) - } catch (error) { - if (error instanceof Error && error.name === 'SecurityError') { - // eslint-disable-next-line no-console - console.warn( - '[localStorage] Unable to get item due to security settings' - ) - } - return null + return null + } +} +export function getItem(key: string) { + try { + return window.localStorage.getItem(key) + } catch (error) { + if (error instanceof Error && error.name === 'SecurityError') { + // eslint-disable-next-line no-console + console.warn('[localStorage] Unable to get item due to security settings') } - }, - setItem: function (key: string, value: string) { - try { - return window.localStorage.setItem(key, value) - } catch (error) { - if (error instanceof Error && error.name === 'SecurityError') { - // eslint-disable-next-line no-console - console.warn( - '[localStorage] Unable to set item due to security settings' - ) - } - return null + return null + } +} +export function setItem(key: string, value: string) { + try { + return window.localStorage.setItem(key, value) + } catch (error) { + if (error instanceof Error && error.name === 'SecurityError') { + // eslint-disable-next-line no-console + console.warn('[localStorage] Unable to set item due to security settings') } - }, + return null + } } diff --git a/site/source/template.html b/site/source/template.html index dbcd70c477..495581e406 100644 --- a/site/source/template.html +++ b/site/source/template.html @@ -65,13 +65,128 @@ html[data-useragent*='Trident'] #js { display: none !important; } + + /* Prevent FOUC effect */ + #js { + opacity: 0; + } + + /* CSS Loader */ + + #loading { + animation: appear 0.6s; + transform: translateY(35vh); + width: 100%; + } + #lds-ellipsis { + margin: auto; + position: relative; + width: 64px; + animation: appear 1.5s; + height: 64px; + } + #lds-ellipsis div { + position: absolute; + top: 27px; + width: 11px; + height: 11px; + border-radius: 50%; + background: rgb(41, 117, 209); + animation-timing-function: cubic-bezier(0, 1, 1, 0); + } + + #lds-ellipsis div:nth-child(1) { + left: 6px; + animation: lds-ellipsis1 0.6s infinite; + } + + #lds-ellipsis div:nth-child(2) { + left: 6px; + animation: lds-ellipsis2 0.6s infinite; + } + + #lds-ellipsis div:nth-child(3) { + left: 26px; + animation: lds-ellipsis2 0.6s infinite; + } + + #lds-ellipsis div:nth-child(4) { + left: 45px; + animation: lds-ellipsis3 0.6s infinite; + } + + @keyframes appear { + from { + opacity: 0; + } + 80% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + @keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } + } + + @keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + } + } + + @keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(19px, 0); + } + } -
+
+ +
+ Un service de l'État français +
+
+
+
+
+
+
+ +
+ + +