From 13bee76234b016c875d7e9f41e328efe6c438ac1 Mon Sep 17 00:00:00 2001 From: Vadorequest Date: Wed, 20 Jan 2021 21:57:51 +0100 Subject: [PATCH] (MAJOR) Massive folder structure refactoring (`v2-mst-aptd-gcms-lcz-sty`), using modular approach and module path aliases (#275) --- .codeclimate.yml | 4 +- .storybook/jsconfig.json | 28 ++ .storybook/main.js | 18 + .storybook/preview.js | 34 +- cypress/README.md | 16 + cypress/integration/app/_sanity/2-customer.ts | 4 +- cypress/integration/app/common/footer.ts | 2 +- cypress/integration/app/common/nav.ts | 2 +- cypress/jsconfig.json | 28 ++ cypress/support/commands.js | 2 +- cypress/tsconfig.json | 23 ++ jest.config.js | 19 + jest.extends.ts | 8 +- next.config.js | 18 +- package.json | 8 +- src/README.md | 213 +++++++++++ src/app/README.md | 10 + .../components}/BrowserPageBootstrap.tsx | 56 +-- .../components}/MultiversalAppBootstrap.tsx | 66 ++-- .../MultiversalGlobalExternalStyles.tsx | 0 .../components}/MultiversalGlobalStyles.tsx | 4 +- .../components}/ServerPageBootstrap.tsx | 15 +- src/{ => app}/constants.ts | 8 +- src/{utils/nextjs => app}/getComponentName.ts | 8 + .../types}/CommonServerSideParams.ts | 0 .../MultiversalAppBootstrapPageProps.ts | 2 +- .../types}/MultiversalAppBootstrapProps.ts | 3 +- src/{types/nextjs => app/types}/StaticPath.ts | 0 .../nextjs => app/types}/StaticPathsOutput.ts | 0 .../nextjs => app/types}/StaticPropsInput.ts | 2 +- src/common/README.md | 9 + .../components/ComponentTemplate.tsx | 0 .../components/animations}/Animated3Dots.tsx | 0 .../components/animations}/AnimatedLoader.tsx | 0 .../animations}/AnimatedTextBubble.tsx | 0 .../components/animations/Loader.tsx | 2 +- .../countryFlags}/EnglishHybridFlag.tsx | 0 .../countryFlags}/EnglishUkFlag.tsx | 0 .../components/countryFlags}/FrenchFlag.tsx | 4 +- .../components/dataDisplay}/AllProducts.tsx | 5 +- .../components/dataDisplay}/Btn.tsx | 12 +- .../components/dataDisplay}/BubbleTimer.tsx | 5 +- .../components/dataDisplay}/Cards.tsx | 0 .../components/dataDisplay}/CircleBtn.tsx | 12 +- .../components/dataDisplay}/Code.tsx | 2 +- .../dataDisplay}/DocumentButton.tsx | 0 .../components/dataDisplay}/EllipsisText.tsx | 0 .../components/dataDisplay}/ExternalLink.tsx | 2 +- .../components/dataDisplay}/LegalContent.tsx | 2 +- .../components/dataDisplay}/LinkButton.tsx | 0 .../components/dataDisplay}/Logo.tsx | 55 +-- .../components/dataDisplay}/Markdown.tsx | 10 +- .../components/dataDisplay}/ProductRow.tsx | 9 +- .../components/dataDisplay}/Products.tsx | 3 +- .../components/dataDisplay}/SimpleTooltip.tsx | 0 .../components/dataDisplay}/SpoilerLink.tsx | 0 .../components/dataDisplay}/Stamp.tsx | 0 .../components/dataDisplay}/Text.tsx | 0 .../components/dataDisplay}/ToggleButton.tsx | 0 .../components/dataDisplay}/Tooltip.tsx | 0 .../rehydration/DisplayOnBrowserMount.tsx | 0 src/{ => common}/hocs/withHOCTemplate.tsx | 0 src/{ => common}/hooks/useHasMounted.tsx | 0 src/{utils/node => common/utils}/fs-utils.ts | 0 src/{ => common}/utils/iframe.ts | 0 .../utils}/ignoreNoisyWarningsHacks.ts | 0 .../assets => common/utils}/logo.test.ts | 0 src/{utils/assets => common/utils}/logo.ts | 5 +- src/{ => common}/utils/mobile.ts | 0 src/{utils/app => common/utils}/redirect.ts | 0 src/{utils/timers => common/utils}/waitFor.ts | 0 .../native-features/example-with-ssg.ts | 2 +- .../native-features/example-with-ssr.ts | 0 src/layouts/README.md | 13 + src/layouts/core/SSG.ts | 146 ++++++++ src/layouts/core/SSR.ts | 135 +++++++ .../core/components}/Footer.tsx | 21 +- .../core/components}/Head.tsx | 16 +- src/layouts/core/components/Layout.tsx | 132 +++++++ .../core/components}/Nav.tsx | 28 +- src/layouts/core/components/PageContainer.tsx | 34 ++ .../core/types}/MultiversalPageProps.ts | 2 +- .../core/types}/OnlyBrowserPageProps.ts | 4 +- .../core/types}/OnlyServerPageProps.ts | 4 +- .../core/types}/PublicHeaders.ts | 0 .../core/types}/SSGPageProps.ts | 4 +- .../core/types}/SSRPageProps.ts | 2 +- .../core/types}/SoftPageProps.ts | 0 .../default/components/DefaultFooter.tsx | 3 + .../default/components/DefaultHead.tsx | 3 + .../default/components/DefaultLayout.tsx | 3 + src/layouts/default/components/DefaultNav.tsx | 3 + .../components/DefaultPageContainer.tsx | 3 + src/layouts/default/defaultSSG.ts | 4 + src/layouts/default/defaultSSR.ts | 4 + .../components}/BuiltInFeaturesSection.tsx | 117 +++--- .../components}/BuiltInFeaturesSidebar.tsx | 41 +- .../components}/BuiltInUtilitiesSection.tsx | 58 ++- .../components}/BuiltInUtilitiesSidebar.tsx | 29 +- src/layouts/demo/components/DemoFooter.tsx | 173 +++++++++ src/layouts/demo/components/DemoHead.tsx | 129 +++++++ .../demo/components/DemoLayout.tsx} | 37 +- src/layouts/demo/components/DemoNav.tsx | 354 ++++++++++++++++++ .../demo/components/DemoPage.tsx} | 6 +- .../demo/components/DemoPageContainer.tsx} | 24 +- .../demo/components/DemoSection.tsx} | 6 +- .../components}/ExternalFeaturesSection.tsx | 16 +- .../demo/components}/IntroductionSection.tsx | 10 +- .../components}/NativeFeaturesSection.tsx | 37 +- .../components}/NativeFeaturesSidebar.tsx | 21 +- .../demo/components}/SidebarFooter.tsx | 5 +- .../demo/components}/SidebarToggle.tsx | 2 +- src/layouts/demo/demoSSG.ts | 146 ++++++++ .../nextjs/SSR.ts => layouts/demo/demoSSR.ts} | 68 ++-- src/modules/README.md | 13 + .../core/amplitude}/amplitude.ts | 9 +- .../amplitude/context}/amplitudeContext.tsx | 0 .../core/amplitude}/hooks/useAmplitude.tsx | 2 +- .../core/amplitude}/types/Amplitude.ts | 0 .../core}/api/convertRequestBodyToJSObject.ts | 2 +- .../core}/api/fetchJSON.test.ts | 0 src/{utils => modules/core}/api/fetchJSON.ts | 0 .../UniversalCookiesManager.browser.test.ts | 2 +- .../UniversalCookiesManager.server.test.ts | 1 - .../UniversalCookiesManager.ts | 6 +- .../core/cookiesManager}/cookies.ts | 0 .../core/cookiesManager}/types/Cookies.ts | 0 src/{utils => modules/core/css}/css.test.ts | 0 src/{utils => modules/core/css}/css.ts | 2 +- src/{ => modules/core/css}/types/CSSStyles.ts | 2 +- .../core/data/contexts}/customerContext.tsx | 2 +- .../core/data/contexts}/datasetContext.tsx | 2 +- .../core/data}/hooks/useCustomer.tsx | 4 +- .../core/data}/hooks/useDataset.tsx | 2 +- .../core}/data/record.test.ts | 0 src/{utils => modules/core}/data/record.ts | 5 +- .../data => modules/core/data/types}/Asset.ts | 0 .../core/data/types}/AssetThumbnail.ts | 0 .../core/data/types}/AssetTransformations.ts | 0 .../core/data/types}/Customer.ts | 2 +- .../core/data/types}/CustomerTheme.ts | 0 .../core/data}/types/GenericObject.ts | 0 .../core/data/types}/GraphCMSDataset.ts | 4 +- .../core/data/types}/GraphCMSSystemFields.ts | 0 src/modules/core/data/types/I18nMarkdown.ts | 6 + .../core/data}/types/I18nRichText.ts | 0 src/modules/core/data/types/I18nString.ts | 4 + .../data => modules/core/data/types}/Link.ts | 2 +- .../data => modules/core/data/types}/Logo.ts | 0 src/{ => modules/core/data}/types/Markdown.ts | 0 .../core/data/types}/Product.ts | 0 src/{ => modules/core/data}/types/RichText.ts | 0 .../core/data}/types/SerializedRecord.ts | 0 .../core/data}/types/SidebarLink.ts | 0 .../data => modules/core/data/types}/Theme.ts | 0 .../core/data/types}/VisibilityStatus.ts | 0 src/{utils/js => modules/core/date}/date.ts | 2 +- .../core/date}/getTimestampsElapsedTime.ts | 0 .../core/date}/timeDifference.ts | 0 src/{ => modules/core/date}/types/DateDay.ts | 0 .../errorHandling}/DefaultErrorLayout.tsx | 3 +- .../core/errorHandling}/ErrorDebug.tsx | 2 +- .../core/fontAwesome/fontAwesome.ts} | 0 .../core/githubActions}/dispatchWorkflow.ts | 6 +- .../githubActions}/dispatchWorkflowByPath.ts | 12 +- .../core/githubActions/types}/GHAWorkflow.ts | 0 .../types}/WorkflowsAPIResponse.ts | 0 .../gql/components}/GraphCMSAsset.test.tsx | 0 .../core/gql/components}/GraphCMSAsset.tsx | 35 +- .../__snapshots__/GraphCMSAsset.test.tsx.snap | 0 .../core}/gql/graphcms.test.ts | 0 src/{utils => modules/core}/gql/graphcms.ts | 0 src/{utils => modules/core}/gql/graphql.ts | 0 .../core/gql}/hocs/withApollo.tsx | 20 +- .../core/gql/types}/ApolloQueryOptions.ts | 0 .../i18n/components}/I18nBtnChangeLocale.tsx | 9 +- .../core/i18n/components}/I18nLink.test.tsx | 3 +- .../core/i18n/components}/I18nLink.tsx | 5 +- .../components}/ToggleLanguagesButton.tsx | 2 +- .../__snapshots__/I18nLink.test.tsx.snap | 0 .../core/i18n/contexts}/i18nContext.tsx | 0 src/{ => modules/core/i18n}/hooks/useI18n.tsx | 2 +- src/{utils => modules/core}/i18n/i18n.ts | 4 +- src/{ => modules/core/i18n}/i18nConfig.js | 0 .../core/i18n/i18nRouter.ts} | 0 .../core}/i18n/i18nextLocize.ts | 1 - .../i18n}/middlewares/localeMiddleware.ts | 7 +- .../core/i18n/types}/I18nLocale.ts | 0 src/{utils => modules/core}/js/array.test.ts | 0 src/{utils => modules/core}/js/array.ts | 0 src/{utils/math => modules/core/js}/random.ts | 0 src/{utils => modules/core}/js/string.test.ts | 0 src/{utils => modules/core}/js/string.ts | 15 +- src/{ => modules/core/js}/types/Float.ts | 0 src/{ => modules/core/js}/types/Integer.ts | 0 src/{utils => modules/core}/js/url.test.ts | 2 +- src/{utils => modules/core}/js/url.ts | 3 +- .../core/lightHouse}/lighthouse.ts | 0 .../components}/PreviewModeBanner.tsx | 12 +- .../contexts}/previewModeContext.tsx | 2 +- .../previewMode}/hooks/usePreviewMode.tsx | 2 +- .../core/previewMode}/previewMode.ts | 0 .../core/previewMode/types}/PreviewData.ts | 0 .../components}/QuickPreviewBanner.tsx | 12 +- .../contexts}/quickPreviewContext.tsx | 0 .../quickPreview}/hooks/useQuickPreview.tsx | 2 +- .../core}/quickPreview/quickPreview.ts | 0 .../core/react/types}/ReactButtonProps.ts | 0 .../core/react/types}/ReactDivProps.ts | 0 .../core/react/types}/ReactLinkProps.ts | 0 .../core/reactSelect}/reactSelect.ts | 2 +- .../core/reactSelect}/types/ReactSelect.ts | 0 .../core/sentry}/sentry.ts | 5 +- .../core/serializeSafe/deserializeSafe.ts | 8 + .../core/serializeSafe/serializeSafe.ts | 9 + .../core/serializeSafe}/types/Flatted.ts | 0 .../core/testing/contexts}/cypressContext.tsx | 0 .../core}/testing/cypress.ts | 0 .../core/testing}/hooks/useCypress.tsx | 2 +- src/{ => modules/core/testing}/mocks/songs.ts | 0 .../core/testing/mocks}/tests-mocks.ts | 0 .../core/testing/tests}/env.test.ts | 0 .../core/testing/toContainObject.ts} | 1 + src/{utils => modules/core/theming}/colors.ts | 0 .../data => modules/core/theming}/theme.ts | 14 +- .../core}/theming/themedComponentColors.ts | 2 +- .../core/theming/types/EmotionTheme.ts} | 3 +- .../contexts}/userConsentContext.tsx | 0 .../core/userConsent}/cookieConsent.ts | 16 +- .../userConsent}/hooks/useUserConsent.tsx | 2 +- .../core/userConsent}/types/UserConsent.ts | 0 .../types/UserSemiPersistentSession.ts | 0 .../core/userSession}/useUserSession.tsx | 2 +- .../core/userSession}/userSessionContext.tsx | 2 +- .../core/vercelCache}/diskCacheStorage.ts | 9 +- .../core/vercelCache}/hybridCache.test.ts | 4 +- .../core/vercelCache}/hybridCache.ts | 8 +- .../core/vercelCache}/memoryCacheStorage.ts | 2 +- .../vercelCache/types}/hybridCacheStorage.ts | 0 src/{utils => modules/core/wdyr}/wdyr.tsx | 0 .../webVitals/types}/NextWebVitalsMetrics.ts | 0 .../types}/NextWebVitalsMetricsReport.ts | 0 src/pages/404.tsx | 30 +- src/pages/README.md | 8 +- .../built-in-features/analytics.tsx | 50 +-- .../built-in-features/animations.tsx | 43 ++- .../built-in-features/cookies-consent.tsx | 51 +-- .../built-in-features/css-in-js.tsx | 41 +- .../built-in-features/docs-site.tsx | 39 +- .../built-in-features/graphql.tsx | 39 +- .../built-in-features/hosting.tsx | 39 +- .../built-in-features/icons.tsx | 39 +- .../built-in-features/index.tsx | 5 +- .../built-in-features/manual-deployments.tsx | 39 +- .../built-in-features/monitoring.tsx | 39 +- .../built-in-features/stages-and-secrets.tsx | 39 +- .../built-in-features/static-i18n.tsx | 47 ++- .../built-in-features/ui-components.tsx | 43 ++- .../built-in-utilities/api.tsx | 39 +- .../built-in-utilities/bundle-analysis.tsx | 39 +- .../built-in-utilities/errors-handling.tsx | 53 ++- .../built-in-utilities/hocs.tsx | 39 +- .../built-in-utilities/hooks.tsx | 41 +- .../built-in-utilities/i18nLink-component.tsx | 53 ++- .../built-in-utilities/index.tsx | 5 +- .../built-in-utilities/interactive-error.tsx | 33 +- .../built-in-utilities/packages-upgrade.tsx | 41 +- .../built-in-utilities/security-audit.tsx | 39 +- .../built-in-utilities/svg-to-react.tsx | 55 ++- .../top-level-500-error.tsx | 31 +- .../tracking-useless-re-renders.tsx | 45 ++- .../[locale]/{examples => demo}/index.tsx | 41 +- .../[[...slug]].tsx | 41 +- .../[albumId].tsx | 55 ++- .../example-with-ssg-and-revalidate.tsx | 66 ++-- .../native-features/example-with-ssg.tsx | 63 ++-- .../native-features/example-with-ssr.tsx | 48 ++- .../native-features/index.tsx | 5 +- src/pages/[locale]/index.tsx | 9 +- src/pages/[locale]/pageTemplateSSG.tsx | 28 +- src/pages/[locale]/pageTemplateSSR.tsx | 28 +- src/pages/[locale]/privacy.tsx | 40 +- src/pages/[locale]/terms.tsx | 46 ++- src/pages/_app.tsx | 24 +- src/pages/_document.tsx | 3 +- src/pages/api/autoRedirectToLocalisedPage.ts | 3 +- src/pages/api/error.test.ts | 9 +- src/pages/api/error.ts | 3 +- src/pages/api/preview.ts | 5 +- src/pages/api/startVercelDeployment.ts | 13 +- src/pages/api/status.test.ts | 9 +- src/pages/api/status.ts | 3 +- src/pages/api/webhooks/deploymentCompleted.ts | 11 +- src/propTypes/GraphCMSAssetPropTypes.ts | 16 - .../GraphCMSAssetTransformationsPropTypes.ts | 6 - src/propTypes/LogoPropTypes.ts | 31 -- src/propTypes/README.md | 16 - src/stories/README.md | 8 + .../nrn/animation/Animated3Dots.stories.tsx | 2 +- .../animation/AnimatedTextBubble.stories.tsx | 2 +- .../nrn/animation/BubbleTimer.stories.tsx | 2 +- src/stories/nrn/animation/Loader.stories.tsx | 2 +- .../nrn/asset/GraphCMSAsset.stories.tsx | 12 +- src/stories/nrn/dataDisplay/Btn.stories.tsx | 2 +- .../nrn/dataDisplay/CircleBtn.stories.tsx | 2 +- src/stories/nrn/dataDisplay/Code.stories.tsx | 2 +- .../dataDisplay/DocumentButton.stories.tsx | 2 +- .../nrn/dataDisplay/ExternalLink.stories.tsx | 2 +- .../nrn/dataDisplay/LinkButton.stories.tsx | 4 +- .../nrn/dataDisplay/Markdown.stories.tsx | 2 +- .../nrn/dataDisplay/SpoilerLink.stories.tsx | 4 +- src/stories/nrn/dataDisplay/Stamp.stories.tsx | 4 +- src/stories/nrn/dataDisplay/Text.stories.tsx | 2 +- .../nrn/dataDisplay/ToggleButton.stories.tsx | 2 +- .../ToggleLanguagesButton.stories.tsx | 2 +- .../nrn/i18n/I18nBtnChangeLocale.stories.tsx | 2 +- src/stories/nrn/i18n/I18nLink.stories.tsx | 2 +- src/stories/nrn/layout/Cards.stories.tsx | 60 +-- .../nrn/layout/DefaultErrorLayout.stories.tsx | 2 +- src/stories/nrn/layout/Footer.stories.tsx | 6 +- .../nrn/layout/LegalContent.stories.tsx | 2 +- src/stories/nrn/layout/Nav.stories.tsx | 2 +- .../nrn/layout/PreviewModeBanner.stories.tsx | 2 +- .../nrn/layout/QuickPreviewBanner.stories.tsx | 2 +- .../nrn/overlay/SimpleTooltip.stories.tsx | 2 +- src/stories/nrn/overlay/Tooltip.stories.tsx | 2 +- .../DisplayOnBrowserMount.stories.tsx | 2 +- src/stories/nrn/text/EllipsisText.stories.tsx | 2 +- src/types/I18nMarkdown.ts | 7 - src/types/I18nString.ts | 5 - src/utils/graphCMSDataset/deserializeSafe.ts | 19 - src/utils/graphCMSDataset/serializeSafe.ts | 23 -- src/utils/nextjs/SSG.ts | 196 ---------- svg-to-react/.gitignore | 1 + {src/svg => svg-to-react}/AnimatedBubble.svg | 0 {src/svg => svg-to-react}/AnimatedLoader.svg | 0 .../AnimatedTextBubble.svg | 0 .../EnglishHybridFlag.svg | 0 {src/svg => svg-to-react}/EnglishUkFlag.svg | 0 {src/svg => svg-to-react}/FrenchFlag.svg | 0 .../utils/svg => svg-to-react}/svgTemplate.ts | 0 tsconfig.json | 24 ++ yarn.lock | 52 ++- 343 files changed, 3200 insertions(+), 1908 deletions(-) create mode 100644 .storybook/jsconfig.json create mode 100644 cypress/jsconfig.json create mode 100644 src/README.md create mode 100644 src/app/README.md rename src/{components/appBootstrap => app/components}/BrowserPageBootstrap.tsx (78%) rename src/{components/appBootstrap => app/components}/MultiversalAppBootstrap.tsx (90%) rename src/{components/appBootstrap => app/components}/MultiversalGlobalExternalStyles.tsx (100%) rename src/{components/appBootstrap => app/components}/MultiversalGlobalStyles.tsx (98%) rename src/{components/appBootstrap => app/components}/ServerPageBootstrap.tsx (70%) rename src/{ => app}/constants.ts (94%) rename src/{utils/nextjs => app}/getComponentName.ts (66%) rename src/{types/nextjs => app/types}/CommonServerSideParams.ts (100%) rename src/{types/nextjs => app/types}/MultiversalAppBootstrapPageProps.ts (83%) rename src/{types/nextjs => app/types}/MultiversalAppBootstrapProps.ts (93%) rename src/{types/nextjs => app/types}/StaticPath.ts (100%) rename src/{types/nextjs => app/types}/StaticPathsOutput.ts (100%) rename src/{types/nextjs => app/types}/StaticPropsInput.ts (84%) create mode 100644 src/common/README.md rename src/{ => common}/components/ComponentTemplate.tsx (100%) rename src/{components/svg => common/components/animations}/Animated3Dots.tsx (100%) rename src/{components/svg => common/components/animations}/AnimatedLoader.tsx (100%) rename src/{components/svg => common/components/animations}/AnimatedTextBubble.tsx (100%) rename src/{ => common}/components/animations/Loader.tsx (88%) rename src/{components/svg => common/components/countryFlags}/EnglishHybridFlag.tsx (100%) rename src/{components/svg => common/components/countryFlags}/EnglishUkFlag.tsx (100%) rename src/{components/svg => common/components/countryFlags}/FrenchFlag.tsx (90%) rename src/{components/data => common/components/dataDisplay}/AllProducts.tsx (90%) rename src/{components/utils => common/components/dataDisplay}/Btn.tsx (93%) rename src/{components/utils => common/components/dataDisplay}/BubbleTimer.tsx (87%) rename src/{components/utils => common/components/dataDisplay}/Cards.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/CircleBtn.tsx (95%) rename src/{components/utils => common/components/dataDisplay}/Code.tsx (96%) rename src/{components/utils => common/components/dataDisplay}/DocumentButton.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/EllipsisText.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/ExternalLink.tsx (96%) rename src/{components/utils => common/components/dataDisplay}/LegalContent.tsx (96%) rename src/{components/utils => common/components/dataDisplay}/LinkButton.tsx (100%) rename src/{components/assets => common/components/dataDisplay}/Logo.tsx (72%) rename src/{components/utils => common/components/dataDisplay}/Markdown.tsx (90%) rename src/{components/data => common/components/dataDisplay}/ProductRow.tsx (81%) rename src/{components/data => common/components/dataDisplay}/Products.tsx (93%) rename src/{components/utils => common/components/dataDisplay}/SimpleTooltip.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/SpoilerLink.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/Stamp.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/Text.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/ToggleButton.tsx (100%) rename src/{components/utils => common/components/dataDisplay}/Tooltip.tsx (100%) rename src/{ => common}/components/rehydration/DisplayOnBrowserMount.tsx (100%) rename src/{ => common}/hocs/withHOCTemplate.tsx (100%) rename src/{ => common}/hooks/useHasMounted.tsx (100%) rename src/{utils/node => common/utils}/fs-utils.ts (100%) rename src/{ => common}/utils/iframe.ts (100%) rename src/{utils/app => common/utils}/ignoreNoisyWarningsHacks.ts (100%) rename src/{utils/assets => common/utils}/logo.test.ts (100%) rename src/{utils/assets => common/utils}/logo.ts (96%) rename src/{ => common}/utils/mobile.ts (100%) rename src/{utils/app => common/utils}/redirect.ts (100%) rename src/{utils/timers => common/utils}/waitFor.ts (100%) rename src/gql/pages/{examples => demo}/native-features/example-with-ssg.ts (81%) rename src/gql/pages/{examples => demo}/native-features/example-with-ssr.ts (100%) create mode 100644 src/layouts/README.md create mode 100644 src/layouts/core/SSG.ts create mode 100644 src/layouts/core/SSR.ts rename src/{components/pageLayouts => layouts/core/components}/Footer.tsx (86%) rename src/{components/pageLayouts => layouts/core/components}/Head.tsx (90%) create mode 100644 src/layouts/core/components/Layout.tsx rename src/{components/pageLayouts => layouts/core/components}/Nav.tsx (93%) create mode 100644 src/layouts/core/components/PageContainer.tsx rename src/{types/pageProps => layouts/core/types}/MultiversalPageProps.ts (93%) rename src/{types/pageProps => layouts/core/types}/OnlyBrowserPageProps.ts (58%) rename src/{types/pageProps => layouts/core/types}/OnlyServerPageProps.ts (72%) rename src/{types/pageProps => layouts/core/types}/PublicHeaders.ts (100%) rename src/{types/pageProps => layouts/core/types}/SSGPageProps.ts (85%) rename src/{types/pageProps => layouts/core/types}/SSRPageProps.ts (90%) rename src/{types/pageProps => layouts/core/types}/SoftPageProps.ts (100%) create mode 100644 src/layouts/default/components/DefaultFooter.tsx create mode 100644 src/layouts/default/components/DefaultHead.tsx create mode 100644 src/layouts/default/components/DefaultLayout.tsx create mode 100644 src/layouts/default/components/DefaultNav.tsx create mode 100644 src/layouts/default/components/DefaultPageContainer.tsx create mode 100644 src/layouts/default/defaultSSG.ts create mode 100644 src/layouts/default/defaultSSR.ts rename src/{components/doc => layouts/demo/components}/BuiltInFeaturesSection.tsx (78%) rename src/{components/doc => layouts/demo/components}/BuiltInFeaturesSidebar.tsx (63%) rename src/{components/doc => layouts/demo/components}/BuiltInUtilitiesSection.tsx (71%) rename src/{components/doc => layouts/demo/components}/BuiltInUtilitiesSidebar.tsx (67%) create mode 100644 src/layouts/demo/components/DemoFooter.tsx create mode 100644 src/layouts/demo/components/DemoHead.tsx rename src/{components/pageLayouts/DefaultLayout.tsx => layouts/demo/components/DemoLayout.tsx} (77%) create mode 100644 src/layouts/demo/components/DemoNav.tsx rename src/{components/doc/DocPage.tsx => layouts/demo/components/DemoPage.tsx} (70%) rename src/{components/pageLayouts/DefaultPageContainer.tsx => layouts/demo/components/DemoPageContainer.tsx} (85%) rename src/{components/doc/DocSection.tsx => layouts/demo/components/DemoSection.tsx} (59%) rename src/{components/doc => layouts/demo/components}/ExternalFeaturesSection.tsx (87%) rename src/{components/doc => layouts/demo/components}/IntroductionSection.tsx (88%) rename src/{components/doc => layouts/demo/components}/NativeFeaturesSection.tsx (90%) rename src/{components/doc => layouts/demo/components}/NativeFeaturesSidebar.tsx (69%) rename src/{components/doc => layouts/demo/components}/SidebarFooter.tsx (91%) rename src/{components/doc => layouts/demo/components}/SidebarToggle.tsx (89%) create mode 100644 src/layouts/demo/demoSSG.ts rename src/{utils/nextjs/SSR.ts => layouts/demo/demoSSR.ts} (66%) create mode 100644 src/modules/README.md rename src/{utils/analytics => modules/core/amplitude}/amplitude.ts (96%) rename src/{stores => modules/core/amplitude/context}/amplitudeContext.tsx (100%) rename src/{ => modules/core/amplitude}/hooks/useAmplitude.tsx (88%) rename src/{ => modules/core/amplitude}/types/Amplitude.ts (100%) rename src/{utils => modules/core}/api/convertRequestBodyToJSObject.ts (92%) rename src/{utils => modules/core}/api/fetchJSON.test.ts (100%) rename src/{utils => modules/core}/api/fetchJSON.ts (100%) rename src/{utils/cookies => modules/core/cookiesManager}/UniversalCookiesManager.browser.test.ts (97%) rename src/{utils/cookies => modules/core/cookiesManager}/UniversalCookiesManager.server.test.ts (99%) rename src/{utils/cookies => modules/core/cookiesManager}/UniversalCookiesManager.ts (98%) rename src/{utils/cookies => modules/core/cookiesManager}/cookies.ts (100%) rename src/{ => modules/core/cookiesManager}/types/Cookies.ts (100%) rename src/{utils => modules/core/css}/css.test.ts (100%) rename src/{utils => modules/core/css}/css.ts (97%) rename src/{ => modules/core/css}/types/CSSStyles.ts (58%) rename src/{stores => modules/core/data/contexts}/customerContext.tsx (91%) rename src/{stores => modules/core/data/contexts}/datasetContext.tsx (88%) rename src/{ => modules/core/data}/hooks/useCustomer.tsx (92%) rename src/{ => modules/core/data}/hooks/useDataset.tsx (94%) rename src/{utils => modules/core}/data/record.test.ts (100%) rename src/{utils => modules/core}/data/record.ts (98%) rename src/{types/data => modules/core/data/types}/Asset.ts (100%) rename src/{types/data => modules/core/data/types}/AssetThumbnail.ts (100%) rename src/{types/data => modules/core/data/types}/AssetTransformations.ts (100%) rename src/{types/data => modules/core/data/types}/Customer.ts (89%) rename src/{types/data => modules/core/data/types}/CustomerTheme.ts (100%) rename src/{ => modules/core/data}/types/GenericObject.ts (100%) rename src/{utils/graphCMSDataset => modules/core/data/types}/GraphCMSDataset.ts (58%) rename src/{types/data => modules/core/data/types}/GraphCMSSystemFields.ts (100%) create mode 100644 src/modules/core/data/types/I18nMarkdown.ts rename src/{ => modules/core/data}/types/I18nRichText.ts (100%) create mode 100644 src/modules/core/data/types/I18nString.ts rename src/{types/data => modules/core/data/types}/Link.ts (69%) rename src/{types/data => modules/core/data/types}/Logo.ts (100%) rename src/{ => modules/core/data}/types/Markdown.ts (100%) rename src/{types/data => modules/core/data/types}/Product.ts (100%) rename src/{ => modules/core/data}/types/RichText.ts (100%) rename src/{ => modules/core/data}/types/SerializedRecord.ts (100%) rename src/{ => modules/core/data}/types/SidebarLink.ts (100%) rename src/{types/data => modules/core/data/types}/Theme.ts (100%) rename src/{types/data => modules/core/data/types}/VisibilityStatus.ts (100%) rename src/{utils/js => modules/core/date}/date.ts (97%) rename src/{utils/time => modules/core/date}/getTimestampsElapsedTime.ts (100%) rename src/{utils/time => modules/core/date}/timeDifference.ts (100%) rename src/{ => modules/core/date}/types/DateDay.ts (100%) rename src/{components/errors => modules/core/errorHandling}/DefaultErrorLayout.tsx (96%) rename src/{components/errors => modules/core/errorHandling}/ErrorDebug.tsx (96%) rename src/{utils/icons/font-awesome.ts => modules/core/fontAwesome/fontAwesome.ts} (100%) rename src/{utils/gitHubActions => modules/core/githubActions}/dispatchWorkflow.ts (95%) rename src/{utils/gitHubActions => modules/core/githubActions}/dispatchWorkflowByPath.ts (88%) rename src/{types/githubActions => modules/core/githubActions/types}/GHAWorkflow.ts (100%) rename src/{types/githubActions => modules/core/githubActions/types}/WorkflowsAPIResponse.ts (100%) rename src/{components/assets => modules/core/gql/components}/GraphCMSAsset.test.tsx (100%) rename src/{components/assets => modules/core/gql/components}/GraphCMSAsset.tsx (80%) rename src/{components/assets => modules/core/gql/components}/__snapshots__/GraphCMSAsset.test.tsx.snap (100%) rename src/{utils => modules/core}/gql/graphcms.test.ts (100%) rename src/{utils => modules/core}/gql/graphcms.ts (100%) rename src/{utils => modules/core}/gql/graphql.ts (100%) rename src/{ => modules/core/gql}/hocs/withApollo.tsx (94%) rename src/{types/gql => modules/core/gql/types}/ApolloQueryOptions.ts (100%) rename src/{components/i18n => modules/core/i18n/components}/I18nBtnChangeLocale.tsx (88%) rename src/{components/i18n => modules/core/i18n/components}/I18nLink.test.tsx (98%) rename src/{components/i18n => modules/core/i18n/components}/I18nLink.tsx (98%) rename src/{components/utils => modules/core/i18n/components}/ToggleLanguagesButton.tsx (99%) rename src/{components/i18n => modules/core/i18n/components}/__snapshots__/I18nLink.test.tsx.snap (100%) rename src/{stores => modules/core/i18n/contexts}/i18nContext.tsx (100%) rename src/{ => modules/core/i18n}/hooks/useI18n.tsx (89%) rename src/{utils => modules/core}/i18n/i18n.ts (97%) rename src/{ => modules/core/i18n}/i18nConfig.js (100%) rename src/{utils/app/router.ts => modules/core/i18n/i18nRouter.ts} (100%) rename src/{utils => modules/core}/i18n/i18nextLocize.ts (99%) rename src/{ => modules/core/i18n}/middlewares/localeMiddleware.ts (95%) rename src/{types/i18n => modules/core/i18n/types}/I18nLocale.ts (100%) rename src/{utils => modules/core}/js/array.test.ts (100%) rename src/{utils => modules/core}/js/array.ts (100%) rename src/{utils/math => modules/core/js}/random.ts (100%) rename src/{utils => modules/core}/js/string.test.ts (100%) rename src/{utils => modules/core}/js/string.ts (80%) rename src/{ => modules/core/js}/types/Float.ts (100%) rename src/{ => modules/core/js}/types/Integer.ts (100%) rename src/{utils => modules/core}/js/url.test.ts (98%) rename src/{utils => modules/core}/js/url.ts (98%) rename src/{utils/quality => modules/core/lightHouse}/lighthouse.ts (100%) rename src/{components/pageLayouts => modules/core/previewMode/components}/PreviewModeBanner.tsx (94%) rename src/{stores => modules/core/previewMode/contexts}/previewModeContext.tsx (91%) rename src/{ => modules/core/previewMode}/hooks/usePreviewMode.tsx (87%) rename src/{utils/nextjs => modules/core/previewMode}/previewMode.ts (100%) rename src/{types/nextjs => modules/core/previewMode/types}/PreviewData.ts (100%) rename src/{components/pageLayouts => modules/core/quickPreview/components}/QuickPreviewBanner.tsx (91%) rename src/{stores => modules/core/quickPreview/contexts}/quickPreviewContext.tsx (100%) rename src/{ => modules/core/quickPreview}/hooks/useQuickPreview.tsx (87%) rename src/{utils => modules/core}/quickPreview/quickPreview.ts (100%) rename src/{types/react => modules/core/react/types}/ReactButtonProps.ts (100%) rename src/{types/react => modules/core/react/types}/ReactDivProps.ts (100%) rename src/{types/react => modules/core/react/types}/ReactLinkProps.ts (100%) rename src/{utils => modules/core/reactSelect}/reactSelect.ts (96%) rename src/{ => modules/core/reactSelect}/types/ReactSelect.ts (100%) rename src/{utils/monitoring => modules/core/sentry}/sentry.ts (97%) create mode 100644 src/modules/core/serializeSafe/deserializeSafe.ts create mode 100644 src/modules/core/serializeSafe/serializeSafe.ts rename src/{ => modules/core/serializeSafe}/types/Flatted.ts (100%) rename src/{stores => modules/core/testing/contexts}/cypressContext.tsx (100%) rename src/{utils => modules/core}/testing/cypress.ts (100%) rename src/{ => modules/core/testing}/hooks/useCypress.tsx (70%) rename src/{ => modules/core/testing}/mocks/songs.ts (100%) rename src/{utils/testing => modules/core/testing/mocks}/tests-mocks.ts (100%) rename src/{utils/env => modules/core/testing/tests}/env.test.ts (100%) rename src/{utils/extend-jest/toContainObject.js => modules/core/testing/toContainObject.ts} (97%) rename src/{utils => modules/core/theming}/colors.ts (100%) rename src/{utils/data => modules/core/theming}/theme.ts (82%) rename src/{utils => modules/core}/theming/themedComponentColors.ts (98%) rename src/{types/emotion-theme.ts => modules/core/theming/types/EmotionTheme.ts} (59%) rename src/{stores => modules/core/userConsent/contexts}/userConsentContext.tsx (100%) rename src/{utils/cookies => modules/core/userConsent}/cookieConsent.ts (97%) rename src/{ => modules/core/userConsent}/hooks/useUserConsent.tsx (90%) rename src/{ => modules/core/userConsent}/types/UserConsent.ts (100%) rename src/{ => modules/core/userSession}/types/UserSemiPersistentSession.ts (100%) rename src/{hooks => modules/core/userSession}/useUserSession.tsx (88%) rename src/{stores => modules/core/userSession}/userSessionContext.tsx (94%) rename src/{utils/caching => modules/core/vercelCache}/diskCacheStorage.ts (95%) rename src/{utils/caching => modules/core/vercelCache}/hybridCache.test.ts (98%) rename src/{utils/caching => modules/core/vercelCache}/hybridCache.ts (96%) rename src/{utils/caching => modules/core/vercelCache}/memoryCacheStorage.ts (96%) rename src/{utils/caching => modules/core/vercelCache/types}/hybridCacheStorage.ts (100%) rename src/{utils => modules/core/wdyr}/wdyr.tsx (100%) rename src/{types/nextjs => modules/core/webVitals/types}/NextWebVitalsMetrics.ts (100%) rename src/{types/nextjs => modules/core/webVitals/types}/NextWebVitalsMetricsReport.ts (100%) rename src/pages/[locale]/{examples => demo}/built-in-features/analytics.tsx (91%) rename src/pages/[locale]/{examples => demo}/built-in-features/animations.tsx (75%) rename src/pages/[locale]/{examples => demo}/built-in-features/cookies-consent.tsx (81%) rename src/pages/[locale]/{examples => demo}/built-in-features/css-in-js.tsx (82%) rename src/pages/[locale]/{examples => demo}/built-in-features/docs-site.tsx (76%) rename src/pages/[locale]/{examples => demo}/built-in-features/graphql.tsx (85%) rename src/pages/[locale]/{examples => demo}/built-in-features/hosting.tsx (78%) rename src/pages/[locale]/{examples => demo}/built-in-features/icons.tsx (81%) rename src/pages/[locale]/{examples => demo}/built-in-features/index.tsx (73%) rename src/pages/[locale]/{examples => demo}/built-in-features/manual-deployments.tsx (83%) rename src/pages/[locale]/{examples => demo}/built-in-features/monitoring.tsx (77%) rename src/pages/[locale]/{examples => demo}/built-in-features/stages-and-secrets.tsx (83%) rename src/pages/[locale]/{examples => demo}/built-in-features/static-i18n.tsx (90%) rename src/pages/[locale]/{examples => demo}/built-in-features/ui-components.tsx (81%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/api.tsx (78%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/bundle-analysis.tsx (71%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/errors-handling.tsx (76%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/hocs.tsx (79%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/hooks.tsx (85%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/i18nLink-component.tsx (68%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/index.tsx (75%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/interactive-error.tsx (78%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/packages-upgrade.tsx (89%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/security-audit.tsx (87%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/svg-to-react.tsx (72%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/top-level-500-error.tsx (72%) rename src/pages/[locale]/{examples => demo}/built-in-utilities/tracking-useless-re-renders.tsx (80%) rename src/pages/[locale]/{examples => demo}/index.tsx (67%) rename src/pages/[locale]/{examples => demo}/native-features/example-optional-catch-all-routes/[[...slug]].tsx (73%) rename src/pages/[locale]/{examples => demo}/native-features/example-with-ssg-and-fallback/[albumId].tsx (81%) rename src/pages/[locale]/{examples => demo}/native-features/example-with-ssg-and-revalidate.tsx (79%) rename src/pages/[locale]/{examples => demo}/native-features/example-with-ssg.tsx (77%) rename src/pages/[locale]/{examples => demo}/native-features/example-with-ssr.tsx (85%) rename src/pages/[locale]/{examples => demo}/native-features/index.tsx (74%) delete mode 100644 src/propTypes/GraphCMSAssetPropTypes.ts delete mode 100644 src/propTypes/GraphCMSAssetTransformationsPropTypes.ts delete mode 100644 src/propTypes/LogoPropTypes.ts delete mode 100644 src/propTypes/README.md create mode 100644 src/stories/README.md delete mode 100644 src/types/I18nMarkdown.ts delete mode 100644 src/types/I18nString.ts delete mode 100644 src/utils/graphCMSDataset/deserializeSafe.ts delete mode 100644 src/utils/graphCMSDataset/serializeSafe.ts delete mode 100644 src/utils/nextjs/SSG.ts create mode 100644 svg-to-react/.gitignore rename {src/svg => svg-to-react}/AnimatedBubble.svg (100%) rename {src/svg => svg-to-react}/AnimatedLoader.svg (100%) rename {src/svg => svg-to-react}/AnimatedTextBubble.svg (100%) rename {src/svg => svg-to-react}/EnglishHybridFlag.svg (100%) rename {src/svg => svg-to-react}/EnglishUkFlag.svg (100%) rename {src/svg => svg-to-react}/FrenchFlag.svg (100%) rename {src/utils/svg => svg-to-react}/svgTemplate.ts (100%) diff --git a/.codeclimate.yml b/.codeclimate.yml index 5e1451c88..a24798cb2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -16,7 +16,7 @@ checks: method-complexity: enabled: true config: - threshold: 10 + threshold: 25 # 10 by default method-count: enabled: true config: @@ -24,7 +24,7 @@ checks: method-lines: enabled: true config: - threshold: 200 # 25 by default + threshold: 300 # 25 by default nested-control-flow: enabled: true config: diff --git a/.storybook/jsconfig.json b/.storybook/jsconfig.json new file mode 100644 index 000000000..84c874b98 --- /dev/null +++ b/.storybook/jsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/app/*": [ + "../src/app/*" + ], + "@/common/*": [ + "../src/common/*" + ], + "@/components/*": [ + "../src/common/components/*" + ], + "@/utils/*": [ + "../src/common/utils/*" + ], + "@/layouts/*": [ + "../src/layouts/*" + ], + "@/modules/*": [ + "../src/modules/*" + ], + "@/pages/*": [ + "../src/pages/*" + ] + } + } +} diff --git a/.storybook/main.js b/.storybook/main.js index 523614f0c..c6d9efe68 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -230,6 +230,24 @@ module.exports = { '@emotion/core': toPath('node_modules/@emotion/react'), '@emotion/styled': toPath('node_modules/@emotion/styled'), 'emotion-theming': toPath('node_modules/@emotion/react'), + + /** + * Map our module path aliases, so that Storybook can understand modules loaded using "@/common" and load the proper file. + * Required, or Storybook will fail to import dependencies from Stories. + * + * XXX The below list must match `tsconfig.json:compilerOptions.paths`, so the Next.js app and Storybook resolve all aliases the same way. + * The paths mapping must also match the `jsconfig.json:compilerOptions.paths` file, which is necessary for WebStorm to understand them for .js files. + * + * @see https://nextjs.org/docs/advanced-features/module-path-aliases + * @see https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003361399/comments/360002636080 + */ + "@/app": path.resolve(__dirname, "../src/app"), + "@/common": path.resolve(__dirname, "../src/common"), + "@/components": path.resolve(__dirname, "../src/common/components"), + "@/utils": path.resolve(__dirname, "../src/common/utils"), + "@/layouts": path.resolve(__dirname, "../src/layouts"), + "@/modules": path.resolve(__dirname, "../src/modules"), + "@/pages": path.resolve(__dirname, "../src/pages"), }, }, }; diff --git a/.storybook/preview.js b/.storybook/preview.js index 4c84e0392..7ac86f816 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -8,23 +8,23 @@ import find from 'lodash.find'; import React from 'react'; import { withNextRouter } from 'storybook-addon-next-router'; import { withPerformance } from 'storybook-addon-performance'; -import '../src/components/appBootstrap/MultiversalGlobalExternalStyles'; // Import the same 3rd party libraries global styles as the pages/_app.tsx (for UI consistency) -import MultiversalGlobalStyles from '../src/components/appBootstrap/MultiversalGlobalStyles'; -import { defaultLocale, getLangFromLocale, supportedLocales } from '../src/i18nConfig'; -import amplitudeContext from '../src/stores/amplitudeContext'; -import customerContext from '../src/stores/customerContext'; -import { cypressContext } from '../src/stores/cypressContext'; -import datasetContext from '../src/stores/datasetContext'; -import i18nContext from '../src/stores/i18nContext'; -import previewModeContext from '../src/stores/previewModeContext'; -import quickPreviewContext from '../src/stores/quickPreviewContext'; -import userConsentContext from '../src/stores/userConsentContext'; -import { userSessionContext } from '../src/stores/userSessionContext'; -import { getAmplitudeInstance } from '../src/utils/analytics/amplitude'; -import '../src/utils/app/ignoreNoisyWarningsHacks'; -import { initCustomerTheme } from '../src/utils/data/theme'; -import i18nextLocize from '../src/utils/i18n/i18nextLocize'; -import '../src/utils/icons/font-awesome'; +import '@/app/components/MultiversalGlobalExternalStyles'; // Import the same 3rd party libraries global styles as the pages/_app.tsx (for UI consistency) +import MultiversalGlobalStyles from '@/app/components/MultiversalGlobalStyles'; +import { defaultLocale, getLangFromLocale, supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; +import customerContext from '@/modules/core/data/contexts/customerContext'; +import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; +import datasetContext from '@/modules/core/data/contexts/datasetContext'; +import i18nContext from '@/modules/core/i18n/contexts/i18nContext'; +import previewModeContext from '@/modules/core/previewMode/contexts/previewModeContext'; +import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; +import userConsentContext from '@/modules/core/userConsent/contexts/userConsentContext'; +import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; +import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; +import '@/common/utils/ignoreNoisyWarningsHacks'; +import { initCustomerTheme } from '@/modules/core/theming/theme'; +import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; +import '@/modules/core/fontAwesome/fontAwesome'; import dataset from './mock/sb-dataset'; // Loads translations from local file cache (Locize) diff --git a/cypress/README.md b/cypress/README.md index 8b402e33b..693d8bf0c 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -51,3 +51,19 @@ The files `cypress/config-*` are used for different purposes. _[Source](https://docs.cypress.io/faq/questions/using-cypress-faq.html#What-are-your-best-practices-for-organizing-tests)_ [Cypress releases "Real World App" (RWA) - Blog post](https://www.cypress.io/blog/2020/06/11/introducing-the-cypress-real-world-app/) + +## Module path alias mapping + +We use module alias path mappings, to avoid using relative paths (e.g: `../../src/common`) but absolute paths (AKA "module paths") instead (e.g: `@/common`). + +Although it's simpler to use, it's harder to configure because it affects several configuration files: +- The paths mapping in `tsconfig.json:compilerOptions.paths` must match those in `../tsconfig.json:compilerOptions.paths` +- They must also match those in `jsconfig.json` file, which is necessary for WebStorm to understand them for .js files. + +If the module path mappings aren't properly set everywhere, it won't work. + +> You can still use relative paths. + +Reference: +- See [Next.js "Module path aliases" documentation](https://nextjs.org/docs/advanced-features/module-path-aliases) +- See [WebStorm issue](https://intellij-support.jetbrains.com/hc/en-us/community/posts/360003361399/comments/360002636080) diff --git a/cypress/integration/app/_sanity/2-customer.ts b/cypress/integration/app/_sanity/2-customer.ts index 3f13f9ab2..5cfb7022f 100644 --- a/cypress/integration/app/_sanity/2-customer.ts +++ b/cypress/integration/app/_sanity/2-customer.ts @@ -1,5 +1,5 @@ -import { Customer } from '../../../../src/types/data/Customer'; -import { CYPRESS_WINDOW_NS } from '../../../../src/utils/testing/cypress'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { CYPRESS_WINDOW_NS } from '@/modules/core/testing/cypress'; describe('Sanity checks > Browser data', () => { /** diff --git a/cypress/integration/app/common/footer.ts b/cypress/integration/app/common/footer.ts index 3e55dcb52..2d04ff9b4 100644 --- a/cypress/integration/app/common/footer.ts +++ b/cypress/integration/app/common/footer.ts @@ -1,4 +1,4 @@ -import { Customer } from '../../../../src/types/data/Customer'; +import { Customer } from '@/modules/core/data/types/Customer'; const baseUrl = Cypress.config().baseUrl; diff --git a/cypress/integration/app/common/nav.ts b/cypress/integration/app/common/nav.ts index 878d2e965..abe668b61 100644 --- a/cypress/integration/app/common/nav.ts +++ b/cypress/integration/app/common/nav.ts @@ -1,4 +1,4 @@ -import { Customer } from '../../../../src/types/data/Customer'; +import { Customer } from '@/modules/core/data/types/Customer'; const baseUrl = Cypress.config().baseUrl; diff --git a/cypress/jsconfig.json b/cypress/jsconfig.json new file mode 100644 index 000000000..84c874b98 --- /dev/null +++ b/cypress/jsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/app/*": [ + "../src/app/*" + ], + "@/common/*": [ + "../src/common/*" + ], + "@/components/*": [ + "../src/common/components/*" + ], + "@/utils/*": [ + "../src/common/utils/*" + ], + "@/layouts/*": [ + "../src/layouts/*" + ], + "@/modules/*": [ + "../src/modules/*" + ], + "@/pages/*": [ + "../src/pages/*" + ] + } + } +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 08637a81a..ba42fe337 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -8,7 +8,7 @@ // https://on.cypress.io/custom-commands // *********************************************** -import { CYPRESS_WINDOW_NS } from '../../src/utils/testing/cypress'; +import { CYPRESS_WINDOW_NS } from '@/modules/core/testing/cypress'; /** * Prepare DOM aliases by fetching the customer data from the browser window and aliasing them for later use. diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index b0ba89e10..509f3f67a 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -6,6 +6,29 @@ "exclude": [], "compilerOptions": { "baseUrl": ".", + "paths": { + "@/app/*": [ + "../src/app/*" + ], + "@/common/*": [ + "../src/common/*" + ], + "@/components/*": [ + "../src/common/components/*" + ], + "@/utils/*": [ + "../src/common/utils/*" + ], + "@/layouts/*": [ + "../src/layouts/*" + ], + "@/modules/*": [ + "../src/modules/*" + ], + "@/pages/*": [ + "../src/pages/*" + ] + }, "types": [ "cypress" ], diff --git a/jest.config.js b/jest.config.js index 9e5720676..d7901f880 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,25 @@ module.exports = { tsconfig: 'tsconfig.jest.json', }, }, + + /** + * Map our module path aliases, so that Jest can understand modules loaded using "@/common" and load the proper file. + * Required, or Jest will fail to import dependencies from tests. + * + * XXX The below list must match `tsconfig.json:compilerOptions.paths`, so the Next.js app and Jest resolve all aliases the same way. + * + * @see https://nextjs.org/docs/advanced-features/module-path-aliases + * @see https://github.com/ilearnio/module-alias/issues/46#issuecomment-546154015 + */ + moduleNameMapper: { + '^@/app/(.*)$': '/src/app/$1', + '^@/common/(.*)$': '/src/common/$1', + '^@/components/(.*)$': '/src/common/components/$1', + '^@/utils/(.*)$': '/src/common/utils/$1', + '^@/layouts/(.*)$': '/src/layouts/$1', + '^@/modules/(.*)$': '/src/modules/$1', + '^@/pages/(.*)$': '/src/pages/$1', + }, modulePathIgnorePatterns: [ '.next/', 'cypress', diff --git a/jest.extends.ts b/jest.extends.ts index c3536931c..96c6f1afe 100644 --- a/jest.extends.ts +++ b/jest.extends.ts @@ -1,4 +1,10 @@ -import { toMatchOneOf, toMatchShapeOf } from 'jest-to-match-shape-of'; // See https://www.npmjs.com/package/jest-to-match-shape-of +// XXX All expect.extend() utilities loaded here will be available for all tests, they also might need to be declared in jest.d.ts +import { + toMatchOneOf, + toMatchShapeOf, +} from 'jest-to-match-shape-of'; // See https://www.npmjs.com/package/jest-to-match-shape-of +// Import utilities that extend Jest "expect" function by themselves +import '@/modules/core/testing/toContainObject'; // Extend Jest "expect" function expect.extend({ diff --git a/next.config.js b/next.config.js index 9f35c82ab..3352584c9 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,7 @@ const bundleAnalyzer = require('@next/bundle-analyzer'); const nextSourceMaps = require('@zeit/next-source-maps'); const packageJson = require('./package'); -const i18nConfig = require('./src/i18nConfig'); +const i18nConfig = require('./src/modules/core/i18n/i18nConfig'); const withSourceMaps = nextSourceMaps(); const withBundleAnalyzer = bundleAnalyzer({ // Run with "yarn analyse:bundle" - See https://www.npmjs.com/package/@next/bundle-analyzer @@ -102,22 +102,6 @@ module.exports = withBundleAnalyzer(withSourceMaps({ async headers() { const headers = []; - // XXX Forbid usage in iframes from external 3rd parties, for non-production site - // This is meant to avoid customers using the preview in their production website, which would incur uncontrolled costs on our end - // Also, our preview env cannot scale considering each request send many airtable API calls and those are rate limited and out of our control - if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { - headers.push({ - source: '/(.*?)', // Match all paths, including "/" - See https://github.com/vercel/next.js/discussions/17991#discussioncomment-112028 - // source: '/:path*', // Match all paths, excluding "/" - headers: [ - { - key: 'Content-Security-Policy', - value: 'frame-ancestors *.stacker.app', - }, - ], - }); - } - console.info('Using headers:', JSON.stringify(headers, null, 2)); return headers; diff --git a/package.json b/package.json index 7bb7a50fd..a90272907 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "analyse:bundle:development": "ANALYZE_BUNDLE=true yarn start", "analyse:bundle:production": ". ./scripts/populate-git-env.sh && ANALYZE_BUNDLE=true next build", "analyse:unused": "next-unused", - "svg": "npx svgr -d src/svg src/svg --ext tsx --template src/utils/svg/svgTemplate.ts", + "svg": "npx svgr -d svg-to-react svg-to-react --ext tsx --template svg-to-react/svgTemplate.ts", "deploy": "yarn deploy:customer1", "deploy:all": "yarn deploy:customer1 && yarn deploy:customer2", "deploy:all:production": "yarn deploy:customer1:production && yarn deploy:customer2:production", @@ -81,6 +81,10 @@ "codemod:update-react-imports": "npx react-codemod update-react-imports src/", "codemod:name-default-component": "npx @next/codemod name-default-component src/", "codemod:withamp-to-config": "npx @next/codemod withamp-to-config src/", + "codemod:module-path-aliases": "yarn codemod:module-path-aliases:src && yarn codemod:module-path-aliases:cypress && yarn codemod:module-path-aliases:sb", + "codemod:module-path-aliases:src": "npx relative-to-alias --src 'src' --alias '@/app' --alias-path './src/app' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/common' --alias-path './src/common' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/components' --alias-path './src/common/components' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/utils' --alias-path './src/common/utils' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/layouts' --alias-path './src/layouts' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/modules' --alias-path './src/modules' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'src' --alias '@/pages' --alias-path './src/pages' --extensions 'js,ts,tsx' --language 'typescript'", + "codemod:module-path-aliases:cypress": "npx relative-to-alias --src 'cypress' --alias '@/app' --alias-path './src/app' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/common' --alias-path './src/common' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/components' --alias-path './src/common/components' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/utils' --alias-path './src/common/utils' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/layouts' --alias-path './src/layouts' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/modules' --alias-path './src/modules' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src 'cypress' --alias '@/pages' --alias-path './src/pages' --extensions 'js,ts,tsx' --language 'typescript'", + "codemod:module-path-aliases:sb": "npx relative-to-alias --src '.storybook' --alias '@/app' --alias-path './src/app' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/common' --alias-path './src/common' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/components' --alias-path './src/common/components' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/utils' --alias-path './src/common/utils' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/layouts' --alias-path './src/layouts' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/modules' --alias-path './src/modules' --extensions 'js,ts,tsx' --language 'typescript' && npx relative-to-alias --src '.storybook' --alias '@/pages' --alias-path './src/pages' --extensions 'js,ts,tsx' --language 'typescript'", "security:audit": "yarn audit", "packages:upgrade": "yarn upgrade-interactive --latest" }, @@ -216,6 +220,7 @@ "@types/lodash.xorby": "4.7.6", "@types/popper.js": "1.11.0", "@types/react": "17.0.0", + "@types/react-dom": "17.0.0", "@types/react-test-renderer": "17.0.0", "@types/reactstrap": "8.7.2", "@types/uuid": "8.3.0", @@ -248,6 +253,7 @@ "node-mocks-http": "1.10.0", "open-cli": "6.0.1", "react-test-renderer": "17.0.1", + "relative-to-alias": "2.0.1", "storybook-addon-designs": "5.4.3", "storybook-addon-next-router": "2.0.3", "storybook-addon-performance": "0.14.0", diff --git a/src/README.md b/src/README.md new file mode 100644 index 000000000..cc325bbb7 --- /dev/null +++ b/src/README.md @@ -0,0 +1,213 @@ +Sources folder +=== + +## What is `src`? + +This folder contains all the source files for your Next.js app. + +This is where you should write most of your code. + +> It also contains the `stories` folder, which isn't used by the Next.js framework, but by Storybook. + +## Folder structure + +Overview of what each folder is about: + +- `app`: Contains code _(components, business logic, types)_ that is being used by the special `pages/_app.tsx` Next.js file. +- `common`: Contains everything that cannot be categorized as a `module`. _See documentation below._ +- `layouts`: Contains the layouts used by pages. _See documentation below._ + - `core`: Contains reusable/extendable code _(components, business logic, data fetching)_ used by other layouts. + - `default`: Default layout that comes built-in with the strict minimum components (Nav, Footer). + _If you use NRN as a boilerplate, that's the layout you should get started with!_ + - `demo`: Layout used by the [Next Right Now demo](https://nrn-v2-mst-aptd-gcms-lcz-sty-c1.vercel.app) pages. + All those pages under `/demo` use the `demo` layout, which contains a custom Nav showing links to each example, and a left menu for easier navigation through the demo. +- `modules`: Contains related pieces of code (components, types, utils) grouped together. _See documentation below._ +- `pages`: Contains Next.js pages and `api` folder. +- `stories`: Contains Storybook stories. + +### `app` folder + +This folder contains the code used to start a Next.js page. + +> As a Next.js app grows, it contains more and more code within `pages/_app`, to such a point where it's difficult to understand all that is happening there. +> +> The goal of this folder is to centralize all the business logic executed by `pages/_app`, while keeping it organized and maintenable. + +This folder contains global styles and "page bootstraps" that are used to initialize the client and server, distinctively. +It also contains constants used thorough the app. + +Read more about "[Application Bootstrap](https://unlyed.github.io/next-right-now/concepts/app-bootstrap)". + +### `common` folder + +The common folder is meant to be a simple and quick way for developers to write code, without having to think about to modularize it. + +Basically, **you should write your code in `common` if**: +- You're building a very simple app. +- You're not familiar with modules or not familiar with the existing code. +- You're in a hurry and just want to do something quickly. +- You don't know if what you're working on is a module, or might become one in the future. +- You don't care about using a modular design approach. +- You don't want to think about building a module yet. +- You're unsure about whether using a module or using the `common` folder. +- Etc. + +Whether you use `common` or `modules` folder **does not matter**. Not really. +Don't stress yourself about "doing it the right way", there is no "right way". + +Modules are meant to group related code together so that it's easier to reason about. +It won't change anything if all your code is in `common`, you can always split it into `modules` once you feel it's useful. + +> In the end of the day, it doesn't matter if you're using a module or `common`, +> what matters is you find your code quickly, and the organization makes sense to you (and your team). +> +> Don't fight with yourself about it, don't waste time, just do what feels right. +> Worst case scenario, you'll change it later. + +### `layouts` folder + +Next.js doesn't come with a built-in "layout" system. + +Next Right Now doesn't enforce any layout organization, but we do provide guidance for it. + +> Remember, if the layout organization doesn't fit your needs, you're free to do it completely differently! + +A layout is composed of elements (Components, utils, etc.) that are used in several pages through your app. +Layouts change the meta components that are used by several pages. + +For instance, Next Right Now comes built-in with the following layouts: +- `core` +- `default` +- `demo` + +#### `core` layout + +The `core` is a bit particular. It's meant to contain code shared between several layouts. + +For instance, you might have a `Nav` component that displays some links in the home page, and other links once connected. +If the business logic behind the `Nav` component is very different, you might prefer to use 2 different components. + +But, if the business logic is similar and only the links themselves are different, +you might prefer to avoid code duplication and have one `core/components/Nav` component that is being configured differently by the other layouts. + +That's the purpose of the `core` folder, to contain such components that are used by other layouts. + +> Although components are the most common way to share code between layouts, you might also need to share ways to fetch data or share page properties. +> +> Those are also good and valid use-cases for using the `core` folder. + +#### `default` layout + +The `default` layout is not actively used by Next Right Now, only the page template files use it. + +It's an easy-to-get-started layout containing only the minimal stuff, and it's meant to be customized to fit your needs. + +> You might also want to rename it to something that makes more sense to you, don't hesitate to do so! + +#### `demo` layout + +The `demo` layout is used by the NRN demo pages. + +> Most people who use Next Right Now as a boilerplate eventually get rid of it, we did (on our private fork). + +It's great to show how to build various pages, and can be used as an inspiration. +It'll be most useful at the beginning, when you learn how to build your own pages. + +It's also a good example on how to use layouts, and why they're interesting! + +For instance, the `demo` layout customises the `Nav`, but uses the same `Footer` as those in the `core` layout. + +#### Adding more layouts? + +You can definitely add more layouts! + +Keep in mind a layout's purpose is to: +- Show different pages similarly (same navigation menu, footer, etc.) +- Prepare `pageProps` similarly for different pages (fetching content from a data source, etc.) + +Their goal is to reduce code duplication and increase maintainability of your app. +Use them as you see fit! + +> Usually, a website with an admin section (behind authentication) would use a different layout for the admin part of the site. + +### `modules` folder + +Modules are and advanced way of organizing your code base. + +> Modules within `src/modules/core` are modules built-in with Next Right Now. +> +> You should create your own modules in `src/` or `src/modules`. Either is fine, it's a matter of taste/opinion. + +They don't change how code is compiled or anything like that, +they're purely a way to organize your code, so it's easier to reason/maintain. + +You should group code together into a module if: +- You feel like it'll improve the understanding or maintainability of the code. +- You now understand (from experience) what is that feature you built a few weeks ago and why it would be beneficial to have a dedicated module for it now. +- You have related code splattered all over `common` (components, utils, hooks, etc.), and it feels like all those pieces should be grouped together. + +Modules are the natural evolution of "big" apps. +Actually, your app doesn't need to be "big" (what's "big" anyway?). + +> Do you remember those days where we had our code splitted between `js` and `css` folders? +> +> When React/Angular/Vue came out, they changed the way we build UI by introducing the concept of "Components", +where JS and CSS code lives together, as a unified Component with all related code within the component folder. +> +> Modules are very similar. + +Here is how we decide (at Unly) if we should use a module: +- It should be composed of, at least, 2 different entities. + - Entities are: Types, Components, Utilities, Hooks, Contexts, etc. +- We know it's small now, but it's going to grow soon. +- Exceptionally, if it's related to a 3rd party (e.g: `modules/sentry`), it can be a module. +- Exceptionally, if we feel like it should be a module, then we go for it (it doesn't really matter anyway!). + +#### Why using a modular design pattern? + +Modules are a different way of organizing your source code, in a very different way compared to the well-known MVC design pattern. + +While MVC (and many other) design pattern groups code together based on "role" of each file, +our approach is different and groups **related** code together. + +MVC is very simple to understand because you don't have to think about it, it's very intuitive, at first. +Therefore, it's beginner-friendly. + +But, **it doesn't scale**. + +When you reach a dozen different features, all that code is grouped together (e.g: "Components") even though it's not related to each other. +On the other hand, when a developer wants to do something, it's often related to a "feature". +But, the code related to the feature is splattered in many folders and sub-folders because of the MVC pattern. + +This makes it much harder to locate the code, and doesn't give an overview of all the related pieces. + +That's why we provide a modular design pattern, for those who wishes to use it. +Although, **the `commons` folder uses a MVC-ish** design pattern: You split your files based on their utility (components, etc.). +And, that's because it's just simpler to comprehend and reason about, at the beginning. + +> In the end, Next Right Now doesn't enforce anything. +> +> You're free to use what we recommend (`common` and `modules`) or do everything in `common`, or everything in `modules`, or something entirely different if you wish to! +> +> Eventually, you do what feels right for you, and that's what really matters. + +### `pages` folder + +The `pages` folder is handled by Next.js [as explained in their documentation](https://nextjs.org/docs/basic-features/pages). + +> Next Right Now doesn't change how `pages` work in any way. + +Because Next Right Now provides localized support, most pages are under the `pages/[locale]` folder. + +#### NRN Demo + +Also, the `/pages/[locale]/demo` folder contains all pages related to the Next Right Now demo. +You may wish to delete it, or keep it around for documentation purpose. + +### `stories` folder + +Contains [Storybook stories](https://storybook.js.org/docs/react/get-started/whats-a-story). + +--- + +> The current folder structure is the result of a community discussion (RFC), [check it out](https://github.com/UnlyEd/next-right-now/discussions/183) to share your feedback! diff --git a/src/app/README.md b/src/app/README.md new file mode 100644 index 000000000..45aba2a2a --- /dev/null +++ b/src/app/README.md @@ -0,0 +1,10 @@ +App +=== + +> Check out the [documentation about the folder structure](../README.md#app-folder) + +Summary: +- This folder is a way to organize what happens in `pages/_app`. +- It also contains app-wide configuration (constants). +- `Multiversal` means code executed "no matter what". +- Next Right Now `pages/_app` split the server and the browser code into 2 different files, to make it easier to understand. diff --git a/src/components/appBootstrap/BrowserPageBootstrap.tsx b/src/app/components/BrowserPageBootstrap.tsx similarity index 78% rename from src/components/appBootstrap/BrowserPageBootstrap.tsx rename to src/app/components/BrowserPageBootstrap.tsx index a5246467e..1ee9dfa0d 100644 --- a/src/components/appBootstrap/BrowserPageBootstrap.tsx +++ b/src/app/components/BrowserPageBootstrap.tsx @@ -1,3 +1,27 @@ +import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; +import { OnlyBrowserPageProps } from '@/layouts/core/types/OnlyBrowserPageProps'; +import { getAmplitudeInstance } from '@/modules/core/amplitude/amplitude'; +import amplitudeContext from '@/modules/core/amplitude/context/amplitudeContext'; +import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; +import useCustomer from '@/modules/core/data/hooks/useCustomer'; +import useDataset from '@/modules/core/data/hooks/useDataset'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { detectLightHouse } from '@/modules/core/lightHouse/lighthouse'; +import { configureSentryUser } from '@/modules/core/sentry/sentry'; +import { cypressContext } from '@/modules/core/testing/contexts/cypressContext'; +import { + CYPRESS_WINDOW_NS, + detectCypress, +} from '@/modules/core/testing/cypress'; +import userConsentContext from '@/modules/core/userConsent/contexts/userConsentContext'; +import initCookieConsent, { getUserConsent } from '@/modules/core/userConsent/cookieConsent'; +import { UserConsent } from '@/modules/core/userConsent/types/UserConsent'; +import { UserSemiPersistentSession } from '@/modules/core/userSession/types/UserSemiPersistentSession'; +import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; +import { + getIframeReferrer, + isRunningInIframe, +} from '@/utils/iframe'; import { Amplitude, AmplitudeProvider, @@ -8,34 +32,10 @@ import { createLogger } from '@unly/utils-simple-logger'; import { AmplitudeClient } from 'amplitude-js'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import useCustomer from '../../hooks/useCustomer'; -import useDataset from '../../hooks/useDataset'; -import amplitudeContext from '../../stores/amplitudeContext'; -import { cypressContext } from '../../stores/cypressContext'; -import userConsentContext from '../../stores/userConsentContext'; -import { userSessionContext } from '../../stores/userSessionContext'; -import { Customer } from '../../types/data/Customer'; -import { MultiversalAppBootstrapPageProps } from '../../types/nextjs/MultiversalAppBootstrapPageProps'; -import { MultiversalAppBootstrapProps } from '../../types/nextjs/MultiversalAppBootstrapProps'; -import { MultiversalPageProps } from '../../types/pageProps/MultiversalPageProps'; -import { OnlyBrowserPageProps } from '../../types/pageProps/OnlyBrowserPageProps'; -import { UserConsent } from '../../types/UserConsent'; -import { UserSemiPersistentSession } from '../../types/UserSemiPersistentSession'; -import { getAmplitudeInstance } from '../../utils/analytics/amplitude'; -import initCookieConsent, { getUserConsent } from '../../utils/cookies/cookieConsent'; -import UniversalCookiesManager from '../../utils/cookies/UniversalCookiesManager'; -import { - getIframeReferrer, - isRunningInIframe, -} from '../../utils/iframe'; -import { configureSentryUser } from '../../utils/monitoring/sentry'; -import { detectLightHouse } from '../../utils/quality/lighthouse'; -import { - CYPRESS_WINDOW_NS, - detectCypress, -} from '../../utils/testing/cypress'; +import { MultiversalAppBootstrapPageProps } from '../types/MultiversalAppBootstrapPageProps'; +import { MultiversalAppBootstrapProps } from '../types/MultiversalAppBootstrapProps'; -const fileLabel = 'components/appBootstrap/BrowserPageBootstrap'; +const fileLabel = 'app/components/BrowserPageBootstrap'; const logger = createLogger({ label: fileLabel, }); @@ -110,7 +110,7 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => allowedPages: [ // We only allow it on those pages to avoid display that boring popup on every page `${window.location.origin}/${locale}/terms`, `${window.location.origin}/${locale}/privacy`, - `${window.location.origin}/${locale}/examples/built-in-features/cookies-consent`, + `${window.location.origin}/${locale}/demo/built-in-features/cookies-consent`, ], amplitudeInstance, locale, diff --git a/src/components/appBootstrap/MultiversalAppBootstrap.tsx b/src/app/components/MultiversalAppBootstrap.tsx similarity index 90% rename from src/components/appBootstrap/MultiversalAppBootstrap.tsx rename to src/app/components/MultiversalAppBootstrap.tsx index cbc914a6a..32b2bbbf5 100644 --- a/src/components/appBootstrap/MultiversalAppBootstrap.tsx +++ b/src/app/components/MultiversalAppBootstrap.tsx @@ -1,3 +1,32 @@ +import Loader from '@/components/animations/Loader'; +import { SSGPageProps } from '@/layouts/core/types/SSGPageProps'; +import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; +import customerContext from '@/modules/core/data/contexts/customerContext'; +import datasetContext from '@/modules/core/data/contexts/datasetContext'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; +import { GraphCMSDataset } from '@/modules/core/data/types/GraphCMSDataset'; +import DefaultErrorLayout from '@/modules/core/errorHandling/DefaultErrorLayout'; +import i18nContext from '@/modules/core/i18n/contexts/i18nContext'; +import { DEFAULT_LOCALE } from '@/modules/core/i18n/i18n'; +import i18nextLocize from '@/modules/core/i18n/i18nextLocize'; +import { + i18nRedirect, + stringifyQueryParameters, +} from '@/modules/core/i18n/i18nRouter'; +import { detectLightHouse } from '@/modules/core/lightHouse/lighthouse'; +import previewModeContext from '@/modules/core/previewMode/contexts/previewModeContext'; +import { + startPreviewMode, + stopPreviewMode, +} from '@/modules/core/previewMode/previewMode'; +import quickPreviewContext from '@/modules/core/quickPreview/contexts/quickPreviewContext'; +import { configureSentryI18n } from '@/modules/core/sentry/sentry'; +import deserializeSafe from '@/modules/core/serializeSafe/deserializeSafe'; +import { detectCypress } from '@/modules/core/testing/cypress'; +import { initCustomerTheme } from '@/modules/core/theming/theme'; +import ErrorPage from '@/pages/_error'; +import { NO_AUTO_PREVIEW_MODE_KEY } from '@/pages/api/preview'; import { ThemeProvider } from '@emotion/react'; import * as Sentry from '@sentry/node'; import { isBrowser } from '@unly/utils'; @@ -8,42 +37,13 @@ import includes from 'lodash.includes'; import isEmpty from 'lodash.isempty'; import size from 'lodash.size'; import React, { useState } from 'react'; -import ErrorPage from '../../pages/_error'; -import { NO_AUTO_PREVIEW_MODE_KEY } from '../../pages/api/preview'; -import customerContext from '../../stores/customerContext'; -import datasetContext from '../../stores/datasetContext'; -import i18nContext from '../../stores/i18nContext'; -import previewModeContext from '../../stores/previewModeContext'; -import quickPreviewContext from '../../stores/quickPreviewContext'; -import { Customer } from '../../types/data/Customer'; -import { CustomerTheme } from '../../types/data/CustomerTheme'; -import { MultiversalAppBootstrapProps } from '../../types/nextjs/MultiversalAppBootstrapProps'; -import { SSGPageProps } from '../../types/pageProps/SSGPageProps'; -import { SSRPageProps } from '../../types/pageProps/SSRPageProps'; -import { - i18nRedirect, - stringifyQueryParameters, -} from '../../utils/app/router'; -import { initCustomerTheme } from '../../utils/data/theme'; -import deserializeSafe from '../../utils/graphCMSDataset/deserializeSafe'; -import { GraphCMSDataset } from '../../utils/graphCMSDataset/GraphCMSDataset'; -import { DEFAULT_LOCALE } from '../../utils/i18n/i18n'; -import i18nextLocize from '../../utils/i18n/i18nextLocize'; -import { configureSentryI18n } from '../../utils/monitoring/sentry'; -import getComponentName from '../../utils/nextjs/getComponentName'; -import { - startPreviewMode, - stopPreviewMode, -} from '../../utils/nextjs/previewMode'; -import { detectLightHouse } from '../../utils/quality/lighthouse'; -import { detectCypress } from '../../utils/testing/cypress'; -import Loader from '../animations/Loader'; -import DefaultErrorLayout from '../errors/DefaultErrorLayout'; +import getComponentName from '../getComponentName'; +import { MultiversalAppBootstrapProps } from '../types/MultiversalAppBootstrapProps'; import BrowserPageBootstrap, { BrowserPageBootstrapProps } from './BrowserPageBootstrap'; import MultiversalGlobalStyles from './MultiversalGlobalStyles'; import ServerPageBootstrap, { ServerPageBootstrapProps } from './ServerPageBootstrap'; -const fileLabel = 'components/appBootstrap/MultiversalAppBootstrap'; +const fileLabel = 'app/components/MultiversalAppBootstrap'; const logger = createLogger({ label: fileLabel, }); @@ -211,7 +211,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } const dataset: GraphCMSDataset = deserializeSafe(serializedDataset); - const customer: Customer = find(dataset, { __typename: 'Customer' }) as Customer; + const customer: Customer = dataset?.customer; let availableLanguages: string[] = customer?.availableLanguages; if (isEmpty(availableLanguages)) { diff --git a/src/components/appBootstrap/MultiversalGlobalExternalStyles.tsx b/src/app/components/MultiversalGlobalExternalStyles.tsx similarity index 100% rename from src/components/appBootstrap/MultiversalGlobalExternalStyles.tsx rename to src/app/components/MultiversalGlobalExternalStyles.tsx diff --git a/src/components/appBootstrap/MultiversalGlobalStyles.tsx b/src/app/components/MultiversalGlobalStyles.tsx similarity index 98% rename from src/components/appBootstrap/MultiversalGlobalStyles.tsx rename to src/app/components/MultiversalGlobalStyles.tsx index 929dc9842..9a77d6307 100644 --- a/src/components/appBootstrap/MultiversalGlobalStyles.tsx +++ b/src/app/components/MultiversalGlobalStyles.tsx @@ -1,10 +1,10 @@ +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; import { css, Global, } from '@emotion/react'; import React from 'react'; -import { NRN_DEFAULT_FALLBACK_FONTS } from '../../constants'; -import { CustomerTheme } from '../../types/data/CustomerTheme'; +import { NRN_DEFAULT_FALLBACK_FONTS } from '../constants'; type Props = { customerTheme: CustomerTheme; diff --git a/src/components/appBootstrap/ServerPageBootstrap.tsx b/src/app/components/ServerPageBootstrap.tsx similarity index 70% rename from src/components/appBootstrap/ServerPageBootstrap.tsx rename to src/app/components/ServerPageBootstrap.tsx index 21ff21dc4..f00e23d82 100644 --- a/src/components/appBootstrap/ServerPageBootstrap.tsx +++ b/src/app/components/ServerPageBootstrap.tsx @@ -1,15 +1,14 @@ +import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; +import { OnlyServerPageProps } from '@/layouts/core/types/OnlyServerPageProps'; +import { configureSentryUser } from '@/modules/core/sentry/sentry'; +import { userSessionContext } from '@/modules/core/userSession/userSessionContext'; import * as Sentry from '@sentry/node'; import { createLogger } from '@unly/utils-simple-logger'; import React from 'react'; +import { MultiversalAppBootstrapPageProps } from '../types/MultiversalAppBootstrapPageProps'; +import { MultiversalAppBootstrapProps } from '../types/MultiversalAppBootstrapProps'; -import { userSessionContext } from '../../stores/userSessionContext'; -import { MultiversalAppBootstrapPageProps } from '../../types/nextjs/MultiversalAppBootstrapPageProps'; -import { MultiversalAppBootstrapProps } from '../../types/nextjs/MultiversalAppBootstrapProps'; -import { MultiversalPageProps } from '../../types/pageProps/MultiversalPageProps'; -import { OnlyServerPageProps } from '../../types/pageProps/OnlyServerPageProps'; -import { configureSentryUser } from '../../utils/monitoring/sentry'; - -const fileLabel = 'components/appBootstrap/ServerPageBootstrap'; +const fileLabel = 'app/components/ServerPageBootstrap'; const logger = createLogger({ label: fileLabel, }); diff --git a/src/constants.ts b/src/app/constants.ts similarity index 94% rename from src/constants.ts rename to src/app/constants.ts index 020a7fd83..f9090614c 100644 --- a/src/constants.ts +++ b/src/app/constants.ts @@ -1,5 +1,5 @@ -import { CustomerTheme } from './types/data/CustomerTheme'; -import { resolveVariantColor } from './utils/colors'; +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; +import { resolveVariantColor } from '@/modules/core/theming/colors'; export const NRN_DEFAULT_SERVICE_LABEL = process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? 'Next Right Now' : `[${process.env.NEXT_PUBLIC_APP_STAGE === 'staging' ? 'Preview' : 'Dev'}] Next Right Now`; @@ -25,9 +25,9 @@ export const NRN_DEFAULT_FONT = 'neuzeit-grotesk'; * Theme applied by default when no theme is defined. * Will be used on a variable-by-variable basis based on what's configured on the CMS for the customer. * - * Applied through "src/utils/airtableSchema/airtableSchema.ts" default value transformations. - * + * Applied through "src/modules/core/theming/theme.ts" default value transformations. * Strongly inspired from Material Design Color System. + * * @see The below documentation comes from https://material.io/design/color/the-color-system.html */ export const NRN_DEFAULT_THEME: Omit & { diff --git a/src/utils/nextjs/getComponentName.ts b/src/app/getComponentName.ts similarity index 66% rename from src/utils/nextjs/getComponentName.ts rename to src/app/getComponentName.ts index 6fe4552a5..cde9d37b7 100644 --- a/src/utils/nextjs/getComponentName.ts +++ b/src/app/getComponentName.ts @@ -3,6 +3,14 @@ import { NextPageContext, } from 'next'; +/** + * Resolves the name of a Next.js "pageProps.Component". + * A "Component" is a "Page". + * + * Extract the name from the component function source code. + * + * @param Component + */ export const getComponentName = (Component: NextComponentType): string | null => { try { const componentAsString = Component.toString(); diff --git a/src/types/nextjs/CommonServerSideParams.ts b/src/app/types/CommonServerSideParams.ts similarity index 100% rename from src/types/nextjs/CommonServerSideParams.ts rename to src/app/types/CommonServerSideParams.ts diff --git a/src/types/nextjs/MultiversalAppBootstrapPageProps.ts b/src/app/types/MultiversalAppBootstrapPageProps.ts similarity index 83% rename from src/types/nextjs/MultiversalAppBootstrapPageProps.ts rename to src/app/types/MultiversalAppBootstrapPageProps.ts index 4e955c571..16a431136 100644 --- a/src/types/nextjs/MultiversalAppBootstrapPageProps.ts +++ b/src/app/types/MultiversalAppBootstrapPageProps.ts @@ -1,5 +1,5 @@ +import { CustomerTheme } from '@/modules/core/data/types/CustomerTheme'; import { i18n } from 'i18next'; -import { CustomerTheme } from '../data/CustomerTheme'; /** * Additional props that are injected by MultiversalAppBootstrap to all pages diff --git a/src/types/nextjs/MultiversalAppBootstrapProps.ts b/src/app/types/MultiversalAppBootstrapProps.ts similarity index 93% rename from src/types/nextjs/MultiversalAppBootstrapProps.ts rename to src/app/types/MultiversalAppBootstrapProps.ts index fb5925a60..ab7a57a9a 100644 --- a/src/types/nextjs/MultiversalAppBootstrapProps.ts +++ b/src/app/types/MultiversalAppBootstrapProps.ts @@ -1,11 +1,10 @@ +import { MultiversalPageProps } from '@/layouts/core/types/MultiversalPageProps'; import { NextComponentType, NextPageContext, } from 'next'; import { Router } from 'next/router'; -import { MultiversalPageProps } from '../pageProps/MultiversalPageProps'; - /** * Props that are provided to the render function of the application (in _app) * Those props can be consolidated by either getInitialProps, getServerProps or getStaticProps, depending on the page and its configuration diff --git a/src/types/nextjs/StaticPath.ts b/src/app/types/StaticPath.ts similarity index 100% rename from src/types/nextjs/StaticPath.ts rename to src/app/types/StaticPath.ts diff --git a/src/types/nextjs/StaticPathsOutput.ts b/src/app/types/StaticPathsOutput.ts similarity index 100% rename from src/types/nextjs/StaticPathsOutput.ts rename to src/app/types/StaticPathsOutput.ts diff --git a/src/types/nextjs/StaticPropsInput.ts b/src/app/types/StaticPropsInput.ts similarity index 84% rename from src/types/nextjs/StaticPropsInput.ts rename to src/app/types/StaticPropsInput.ts index 8191cc6ca..6efacba4c 100644 --- a/src/types/nextjs/StaticPropsInput.ts +++ b/src/app/types/StaticPropsInput.ts @@ -1,5 +1,5 @@ +import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; import { CommonServerSideParams } from './CommonServerSideParams'; -import { PreviewData } from './PreviewData'; /** * Static props given as inputs for getStaticProps diff --git a/src/common/README.md b/src/common/README.md new file mode 100644 index 000000000..05cd378d7 --- /dev/null +++ b/src/common/README.md @@ -0,0 +1,9 @@ +Common +=== + +> Check out the [documentation about the folder structure](../README.md#common-folder) + +Summary: +- This folder uses an MVC-ish design pattern, where you split your files in separated folders, depending on their kind. +- This folder is great to quickly write some piece of code, you don't need to think a lot about how organized your code should be, and can get started quickly. +- If you don't know or are unsure whether to go for `common` or `modules`, pick `common`. You can always change your mind later. diff --git a/src/components/ComponentTemplate.tsx b/src/common/components/ComponentTemplate.tsx similarity index 100% rename from src/components/ComponentTemplate.tsx rename to src/common/components/ComponentTemplate.tsx diff --git a/src/components/svg/Animated3Dots.tsx b/src/common/components/animations/Animated3Dots.tsx similarity index 100% rename from src/components/svg/Animated3Dots.tsx rename to src/common/components/animations/Animated3Dots.tsx diff --git a/src/components/svg/AnimatedLoader.tsx b/src/common/components/animations/AnimatedLoader.tsx similarity index 100% rename from src/components/svg/AnimatedLoader.tsx rename to src/common/components/animations/AnimatedLoader.tsx diff --git a/src/components/svg/AnimatedTextBubble.tsx b/src/common/components/animations/AnimatedTextBubble.tsx similarity index 100% rename from src/components/svg/AnimatedTextBubble.tsx rename to src/common/components/animations/AnimatedTextBubble.tsx diff --git a/src/components/animations/Loader.tsx b/src/common/components/animations/Loader.tsx similarity index 88% rename from src/components/animations/Loader.tsx rename to src/common/components/animations/Loader.tsx index b83356855..54c47abe5 100644 --- a/src/components/animations/Loader.tsx +++ b/src/common/components/animations/Loader.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import React from 'react'; -import AnimatedLoader from '../svg/AnimatedLoader'; +import AnimatedLoader from './AnimatedLoader'; export type Props = {} diff --git a/src/components/svg/EnglishHybridFlag.tsx b/src/common/components/countryFlags/EnglishHybridFlag.tsx similarity index 100% rename from src/components/svg/EnglishHybridFlag.tsx rename to src/common/components/countryFlags/EnglishHybridFlag.tsx diff --git a/src/components/svg/EnglishUkFlag.tsx b/src/common/components/countryFlags/EnglishUkFlag.tsx similarity index 100% rename from src/components/svg/EnglishUkFlag.tsx rename to src/common/components/countryFlags/EnglishUkFlag.tsx diff --git a/src/components/svg/FrenchFlag.tsx b/src/common/components/countryFlags/FrenchFlag.tsx similarity index 90% rename from src/components/svg/FrenchFlag.tsx rename to src/common/components/countryFlags/FrenchFlag.tsx index 4cf1d8bf9..db593e56b 100644 --- a/src/components/svg/FrenchFlag.tsx +++ b/src/common/components/countryFlags/FrenchFlag.tsx @@ -1,8 +1,10 @@ -import React from "react"; +import React from 'react'; + type Props = {} & React.SVGProps; const SvgFrenchFlag = (props: Props): JSX.Element => { return ( + // @ts-ignore = (props) => { map(products, (productInStage: { documentInStages: Product[] } & Product) => { const productsInStage: Product[] = productInStage?.documentInStages; // Contains an array of 1 element, when there is a PUBLISHED record (otherwise is empty) map(productsInStage, (product: Product) => { - if(product?.stage === 'PUBLISHED'){ + if (product?.stage === 'PUBLISHED') { productsPublished.push(product); } }); diff --git a/src/components/utils/Btn.tsx b/src/common/components/dataDisplay/Btn.tsx similarity index 93% rename from src/components/utils/Btn.tsx rename to src/common/components/dataDisplay/Btn.tsx index b1439b6cc..8ac96e67d 100644 --- a/src/components/utils/Btn.tsx +++ b/src/common/components/dataDisplay/Btn.tsx @@ -1,15 +1,15 @@ +import { ReactButtonProps } from '@/modules/core/react/types/ReactButtonProps'; +import { + ComponentThemeMode, + resolveThemedComponentColors, + ThemedComponentProps, +} from '@/modules/core/theming/themedComponentColors'; import { css, useTheme, } from '@emotion/react'; import classnames from 'classnames'; import React, { ReactNode } from 'react'; -import { ReactButtonProps } from '../../types/react/ReactButtonProps'; -import { - ComponentThemeMode, - resolveThemedComponentColors, - ThemedComponentProps, -} from '../../utils/theming/themedComponentColors'; export type Props = { /** diff --git a/src/components/utils/BubbleTimer.tsx b/src/common/components/dataDisplay/BubbleTimer.tsx similarity index 87% rename from src/components/utils/BubbleTimer.tsx rename to src/common/components/dataDisplay/BubbleTimer.tsx index 47bd1aa44..04f5c36cd 100644 --- a/src/components/utils/BubbleTimer.tsx +++ b/src/common/components/dataDisplay/BubbleTimer.tsx @@ -1,11 +1,10 @@ +import { CSSStyles } from '@/modules/core/css/types/CSSStyles'; import React, { Fragment, useEffect, useState, } from 'react'; - -import { CSSStyles } from '../../types/CSSStyles'; -import AnimatedTextBubble from '../svg/AnimatedTextBubble'; +import AnimatedTextBubble from '../animations/AnimatedTextBubble'; export type Props = { children: React.ReactElement; diff --git a/src/components/utils/Cards.tsx b/src/common/components/dataDisplay/Cards.tsx similarity index 100% rename from src/components/utils/Cards.tsx rename to src/common/components/dataDisplay/Cards.tsx diff --git a/src/components/utils/CircleBtn.tsx b/src/common/components/dataDisplay/CircleBtn.tsx similarity index 95% rename from src/components/utils/CircleBtn.tsx rename to src/common/components/dataDisplay/CircleBtn.tsx index 21a173778..d7349a188 100644 --- a/src/components/utils/CircleBtn.tsx +++ b/src/common/components/dataDisplay/CircleBtn.tsx @@ -1,14 +1,14 @@ +import { ReactDivProps } from '@/modules/core/react/types/ReactDivProps'; +import { + ComponentThemeMode, + resolveThemedComponentColors, + ThemedComponentProps, +} from '@/modules/core/theming/themedComponentColors'; import { css, useTheme, } from '@emotion/react'; import React from 'react'; -import { ReactDivProps } from '../../types/react/ReactDivProps'; -import { - ComponentThemeMode, - resolveThemedComponentColors, - ThemedComponentProps, -} from '../../utils/theming/themedComponentColors'; export type Props = { /** diff --git a/src/components/utils/Code.tsx b/src/common/components/dataDisplay/Code.tsx similarity index 96% rename from src/components/utils/Code.tsx rename to src/common/components/dataDisplay/Code.tsx index 0ab2ee2de..050d39c29 100644 --- a/src/components/utils/Code.tsx +++ b/src/common/components/dataDisplay/Code.tsx @@ -1,10 +1,10 @@ +import { CSSStyles } from '@/modules/core/css/types/CSSStyles'; import React from 'react'; import { CodeBlock, dracula, } from 'react-code-blocks'; import { CodeBlockProps } from 'react-code-blocks/dist/components/CodeBlock'; -import { CSSStyles } from '../../types/CSSStyles'; export type Props = { /** diff --git a/src/components/utils/DocumentButton.tsx b/src/common/components/dataDisplay/DocumentButton.tsx similarity index 100% rename from src/components/utils/DocumentButton.tsx rename to src/common/components/dataDisplay/DocumentButton.tsx diff --git a/src/components/utils/EllipsisText.tsx b/src/common/components/dataDisplay/EllipsisText.tsx similarity index 100% rename from src/components/utils/EllipsisText.tsx rename to src/common/components/dataDisplay/EllipsisText.tsx diff --git a/src/components/utils/ExternalLink.tsx b/src/common/components/dataDisplay/ExternalLink.tsx similarity index 96% rename from src/components/utils/ExternalLink.tsx rename to src/common/components/dataDisplay/ExternalLink.tsx index ca5770ce3..1c8db4268 100644 --- a/src/components/utils/ExternalLink.tsx +++ b/src/common/components/dataDisplay/ExternalLink.tsx @@ -1,5 +1,5 @@ +import { ReactLinkProps } from '@/modules/core/react/types/ReactLinkProps'; import React, { Fragment } from 'react'; -import { ReactLinkProps } from '../../types/react/ReactLinkProps'; export type Props = { /** diff --git a/src/components/utils/LegalContent.tsx b/src/common/components/dataDisplay/LegalContent.tsx similarity index 96% rename from src/components/utils/LegalContent.tsx rename to src/common/components/dataDisplay/LegalContent.tsx index 25c6d3d05..cd2755581 100644 --- a/src/components/utils/LegalContent.tsx +++ b/src/common/components/dataDisplay/LegalContent.tsx @@ -1,10 +1,10 @@ +import { Markdown as TextAsMarkdown } from '@/modules/core/data/types/Markdown'; import { css, useTheme, } from '@emotion/react'; import React from 'react'; import { Container } from 'reactstrap'; -import { Markdown as TextAsMarkdown } from '../../types/Markdown'; import Markdown from './Markdown'; export type Props = { diff --git a/src/components/utils/LinkButton.tsx b/src/common/components/dataDisplay/LinkButton.tsx similarity index 100% rename from src/components/utils/LinkButton.tsx rename to src/common/components/dataDisplay/LinkButton.tsx diff --git a/src/components/assets/Logo.tsx b/src/common/components/dataDisplay/Logo.tsx similarity index 72% rename from src/components/assets/Logo.tsx rename to src/common/components/dataDisplay/Logo.tsx index 10d8d94d1..87caba0b0 100644 --- a/src/components/assets/Logo.tsx +++ b/src/common/components/dataDisplay/Logo.tsx @@ -1,24 +1,20 @@ -import styled from '@emotion/styled'; -import classnames from 'classnames'; -import deepmerge from 'deepmerge'; -import get from 'lodash.get'; -import PropTypes from 'prop-types'; -// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars -import React from 'react'; -import stylePropType from 'react-style-proptype'; - -import LogoPropTypes from '../../propTypes/LogoPropTypes'; -import { CSSStyles } from '../../types/CSSStyles'; -import { Asset } from '../../types/data/Asset'; -import { Link } from '../../types/data/Link'; -import { Logo as LogoType } from '../../types/data/Logo'; +import { CSSStyles } from '@/modules/core/css/types/CSSStyles'; +import { Asset } from '@/modules/core/data/types/Asset'; +import { Link } from '@/modules/core/data/types/Link'; +import { Logo as LogoType } from '@/modules/core/data/types/Logo'; import { DEFAULT_SIZES_MULTIPLIERS, generateSizes, resolveSize, SizeMultiplier, toPixels, -} from '../../utils/assets/logo'; +} from '@/utils/logo'; +import styled from '@emotion/styled'; +import classnames from 'classnames'; +import deepmerge from 'deepmerge'; +import get from 'lodash.get'; +// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars +import React from 'react'; type Props = { id: string; @@ -56,7 +52,14 @@ const Logo = (props: Props): JSX.Element => { onClick = null, }: Props = props; let resolvedLogoProps: LogoType = deepmerge.all([defaults, logo || {}, override]) as Asset; - resolvedLogoProps = deepmerge.all([resolvedLogoProps, resolveSize({ logo: resolvedLogoProps, width: toPixels(width), height: toPixels(height) })]) as Asset; + resolvedLogoProps = deepmerge.all([ + resolvedLogoProps, + resolveSize({ + logo: resolvedLogoProps, + width: toPixels(width), + height: toPixels(height), + }), + ]) as Asset; // Handle v2 structure (graphcms) if (resolvedLogoProps.linkUrl) { @@ -126,24 +129,4 @@ const Logo = (props: Props): JSX.Element => { } }; -Logo.propTypes = { - id: PropTypes.string.isRequired, - logo: PropTypes.shape(LogoPropTypes), - width: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string, - ]), - height: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string, - ]), - defaults: PropTypes.shape(LogoPropTypes), // Merged with the logo, takes lowest priority - override: PropTypes.shape(LogoPropTypes), // Merged with the logo, takes highest priority - sizesMultipliers: PropTypes.array, - className: PropTypes.string, - style: stylePropType, - link: PropTypes.object, - onClick: PropTypes.func, // Support for usage within component (from Next.js) -}; - export default Logo; diff --git a/src/components/utils/Markdown.tsx b/src/common/components/dataDisplay/Markdown.tsx similarity index 90% rename from src/components/utils/Markdown.tsx rename to src/common/components/dataDisplay/Markdown.tsx index 005911f15..b016cb863 100644 --- a/src/components/utils/Markdown.tsx +++ b/src/common/components/dataDisplay/Markdown.tsx @@ -1,3 +1,7 @@ +import { CSSStyles } from '@/modules/core/css/types/CSSStyles'; +import { Markdown as MarkdownType } from '@/modules/core/data/types/Markdown'; +import I18nBtnChangeLocale from '@/modules/core/i18n/components/I18nBtnChangeLocale'; +import I18nLink from '@/modules/core/i18n/components/I18nLink'; import * as Sentry from '@sentry/node'; import { createLogger } from '@unly/utils-simple-logger'; import classnames from 'classnames'; @@ -17,14 +21,10 @@ import { Row, UncontrolledDropdown as Dropdown, } from 'reactstrap'; -import { CSSStyles } from '../../types/CSSStyles'; -import { Markdown as MarkdownType } from '../../types/Markdown'; -import I18nBtnChangeLocale from '../i18n/I18nBtnChangeLocale'; -import I18nLink from '../i18n/I18nLink'; import Btn from './Btn'; import Tooltip from './SimpleTooltip'; -const fileLabel = 'components/utils/Markdown'; +const fileLabel = 'common/components/dataDisplay/Markdown'; const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript-eslint/no-unused-vars label: fileLabel, }); diff --git a/src/components/data/ProductRow.tsx b/src/common/components/dataDisplay/ProductRow.tsx similarity index 81% rename from src/components/data/ProductRow.tsx rename to src/common/components/dataDisplay/ProductRow.tsx index f06fa7f2b..ba374c770 100644 --- a/src/components/data/ProductRow.tsx +++ b/src/common/components/dataDisplay/ProductRow.tsx @@ -1,14 +1,13 @@ +import { Asset } from '@/modules/core/data/types/Asset'; +import { Product } from '@/modules/core/data/types/Product'; +import GraphCMSAsset from '@/modules/core/gql/components/GraphCMSAsset'; import { css } from '@emotion/react'; import React from 'react'; import { Col, Row, } from 'reactstrap'; - -import { Asset } from '../../types/data/Asset'; -import { Product } from '../../types/data/Product'; -import GraphCMSAsset from '../assets/GraphCMSAsset'; -import Markdown from '../utils/Markdown'; +import Markdown from './Markdown'; type Props = { product: Product; diff --git a/src/components/data/Products.tsx b/src/common/components/dataDisplay/Products.tsx similarity index 93% rename from src/components/data/Products.tsx rename to src/common/components/dataDisplay/Products.tsx index ba22c0318..b179ca35a 100644 --- a/src/components/data/Products.tsx +++ b/src/common/components/dataDisplay/Products.tsx @@ -1,9 +1,8 @@ +import { Product } from '@/modules/core/data/types/Product'; import { css } from '@emotion/react'; import map from 'lodash.map'; import React from 'react'; import { Container } from 'reactstrap'; - -import { Product } from '../../types/data/Product'; import ProductRow from './ProductRow'; type Props = { diff --git a/src/components/utils/SimpleTooltip.tsx b/src/common/components/dataDisplay/SimpleTooltip.tsx similarity index 100% rename from src/components/utils/SimpleTooltip.tsx rename to src/common/components/dataDisplay/SimpleTooltip.tsx diff --git a/src/components/utils/SpoilerLink.tsx b/src/common/components/dataDisplay/SpoilerLink.tsx similarity index 100% rename from src/components/utils/SpoilerLink.tsx rename to src/common/components/dataDisplay/SpoilerLink.tsx diff --git a/src/components/utils/Stamp.tsx b/src/common/components/dataDisplay/Stamp.tsx similarity index 100% rename from src/components/utils/Stamp.tsx rename to src/common/components/dataDisplay/Stamp.tsx diff --git a/src/components/utils/Text.tsx b/src/common/components/dataDisplay/Text.tsx similarity index 100% rename from src/components/utils/Text.tsx rename to src/common/components/dataDisplay/Text.tsx diff --git a/src/components/utils/ToggleButton.tsx b/src/common/components/dataDisplay/ToggleButton.tsx similarity index 100% rename from src/components/utils/ToggleButton.tsx rename to src/common/components/dataDisplay/ToggleButton.tsx diff --git a/src/components/utils/Tooltip.tsx b/src/common/components/dataDisplay/Tooltip.tsx similarity index 100% rename from src/components/utils/Tooltip.tsx rename to src/common/components/dataDisplay/Tooltip.tsx diff --git a/src/components/rehydration/DisplayOnBrowserMount.tsx b/src/common/components/rehydration/DisplayOnBrowserMount.tsx similarity index 100% rename from src/components/rehydration/DisplayOnBrowserMount.tsx rename to src/common/components/rehydration/DisplayOnBrowserMount.tsx diff --git a/src/hocs/withHOCTemplate.tsx b/src/common/hocs/withHOCTemplate.tsx similarity index 100% rename from src/hocs/withHOCTemplate.tsx rename to src/common/hocs/withHOCTemplate.tsx diff --git a/src/hooks/useHasMounted.tsx b/src/common/hooks/useHasMounted.tsx similarity index 100% rename from src/hooks/useHasMounted.tsx rename to src/common/hooks/useHasMounted.tsx diff --git a/src/utils/node/fs-utils.ts b/src/common/utils/fs-utils.ts similarity index 100% rename from src/utils/node/fs-utils.ts rename to src/common/utils/fs-utils.ts diff --git a/src/utils/iframe.ts b/src/common/utils/iframe.ts similarity index 100% rename from src/utils/iframe.ts rename to src/common/utils/iframe.ts diff --git a/src/utils/app/ignoreNoisyWarningsHacks.ts b/src/common/utils/ignoreNoisyWarningsHacks.ts similarity index 100% rename from src/utils/app/ignoreNoisyWarningsHacks.ts rename to src/common/utils/ignoreNoisyWarningsHacks.ts diff --git a/src/utils/assets/logo.test.ts b/src/common/utils/logo.test.ts similarity index 100% rename from src/utils/assets/logo.test.ts rename to src/common/utils/logo.test.ts diff --git a/src/utils/assets/logo.ts b/src/common/utils/logo.ts similarity index 96% rename from src/utils/assets/logo.ts rename to src/common/utils/logo.ts index 591bbeed9..1eafbd7dc 100644 --- a/src/utils/assets/logo.ts +++ b/src/common/utils/logo.ts @@ -1,8 +1,7 @@ +import { GenericObject } from '@/modules/core/data/types/GenericObject'; +import { Logo } from '@/modules/core/data/types/Logo'; import map from 'lodash.map'; -import { Logo } from '../../types/data/Logo'; -import { GenericObject } from '../../types/GenericObject'; - export const SIZE_XS = 'xs'; export const SIZE_SM = 'sm'; export const SIZE_MD = 'md'; diff --git a/src/utils/mobile.ts b/src/common/utils/mobile.ts similarity index 100% rename from src/utils/mobile.ts rename to src/common/utils/mobile.ts diff --git a/src/utils/app/redirect.ts b/src/common/utils/redirect.ts similarity index 100% rename from src/utils/app/redirect.ts rename to src/common/utils/redirect.ts diff --git a/src/utils/timers/waitFor.ts b/src/common/utils/waitFor.ts similarity index 100% rename from src/utils/timers/waitFor.ts rename to src/common/utils/waitFor.ts diff --git a/src/gql/pages/examples/native-features/example-with-ssg.ts b/src/gql/pages/demo/native-features/example-with-ssg.ts similarity index 81% rename from src/gql/pages/examples/native-features/example-with-ssg.ts rename to src/gql/pages/demo/native-features/example-with-ssg.ts index 281b7b252..e38f7aa68 100644 --- a/src/gql/pages/examples/native-features/example-with-ssg.ts +++ b/src/gql/pages/demo/native-features/example-with-ssg.ts @@ -2,7 +2,7 @@ import gql from 'graphql-tag'; import { product } from '../../../fragments/product'; /** - * Used by /src/pages/[locale]/examples/native-features/example-with-ssg page + * Used by /src/pages/[locale]/demo/native-features/example-with-ssg page */ export const EXAMPLE_WITH_SSG_QUERY = gql` query EXAMPLE_WITH_SSG_QUERY($customerRef: String!){ diff --git a/src/gql/pages/examples/native-features/example-with-ssr.ts b/src/gql/pages/demo/native-features/example-with-ssr.ts similarity index 100% rename from src/gql/pages/examples/native-features/example-with-ssr.ts rename to src/gql/pages/demo/native-features/example-with-ssr.ts diff --git a/src/layouts/README.md b/src/layouts/README.md new file mode 100644 index 000000000..8e4ceefec --- /dev/null +++ b/src/layouts/README.md @@ -0,0 +1,13 @@ +Page layouts +=== + +> Check out the [documentation about the folder structure](../README.md#folder-structure) + +Summary: +- `core`: Share reusable components between layouts. +- `default`: Used by all non-demo pages. Customise it, or change the core, as you prefer. +- `demo`: Used by all demo pages. You'll eventually get rid of it, but until then it can be a good inspiration. +- You can add custom layouts and use them in your pages right away. +- Layouts are flexible, we used the `DemoLayout` in all pages under `pages/[locale]/demo` but **you don't have to**, it was a choice. +- Layouts are usually useful when you want to have a similar UI shared by several pages. +- Layouts are meant to avoid code duplication and increase code maintainability. diff --git a/src/layouts/core/SSG.ts b/src/layouts/core/SSG.ts new file mode 100644 index 000000000..6ffa4cde3 --- /dev/null +++ b/src/layouts/core/SSG.ts @@ -0,0 +1,146 @@ +import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; +import { StaticPath } from '@/app/types/StaticPath'; +import { StaticPathsOutput } from '@/app/types/StaticPathsOutput'; +import { StaticPropsInput } from '@/app/types/StaticPropsInput'; +import { Customer } from '@/modules/core/data/types/Customer'; +import { prepareGraphCMSLocaleHeader } from '@/modules/core/gql/graphcms'; +import createApolloClient from '@/modules/core/gql/graphql'; +import { + DEFAULT_LOCALE, + resolveFallbackLanguage, +} from '@/modules/core/i18n/i18n'; +import { supportedLocales } from '@/modules/core/i18n/i18nConfig'; +import { + fetchTranslations, + I18nextResources, +} from '@/modules/core/i18n/i18nextLocize'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { PreviewData } from '@/modules/core/previewMode/types/PreviewData'; +import serializeSafe from '@/modules/core/serializeSafe/serializeSafe'; +import { ApolloQueryResult } from 'apollo-client'; +import map from 'lodash.map'; +import { + GetStaticPaths, + GetStaticPathsContext, + GetStaticProps, + GetStaticPropsResult, +} from 'next'; +import { LAYOUT_QUERY } from '../../gql/common/layoutQuery'; +import { SSGPageProps } from './types/SSGPageProps'; + +/** + * Only executed on the server side at build time. + * Computes all static paths that should be available for all SSG pages. + * Necessary when a page has dynamic routes and uses "getStaticProps", in order to build the HTML pages. + * + * You can use "fallback" option to avoid building all page variants and allow runtime fallback. + * + * Meant to avoid code duplication. + * Can be overridden for per-page customisation (e.g: deepmerge). + * + * XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly. + * + * @return Static paths that will be used by "getCoreStaticProps" to generate pages + * + * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation + */ +export const getCoreStaticPaths: GetStaticPaths = async (context: GetStaticPathsContext): Promise => { + const paths: StaticPath[] = map(supportedLocales, (supportedLocale: I18nLocale): StaticPath => { + return { + params: { + locale: supportedLocale.name, + }, + }; + }); + + return { + fallback: false, + paths, + }; +}; + +/** + * Only executed on the server side at build time. + * Computes all static props that should be available for all SSG pages. + * + * Note that when a page uses "getStaticProps", then "_app:getInitialProps" is executed (if defined) but not actually used by the page, + * only the results from getStaticProps are actually injected into the page (as "SSGPageProps"). + * + * Meant to avoid code duplication. + * Can be overridden for per-page customisation (e.g: deepmerge). + * + * XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly. + * + * @return Props (as "SSGPageProps") that will be passed to the Page component, as props (known as "pageProps" in _app). + * + * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 + * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation + */ +export const getCoreStaticProps: GetStaticProps = async (props: StaticPropsInput): Promise> => { + const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF; + const preview: boolean = props?.preview || false; + const previewData: PreviewData = props?.previewData || null; + const hasLocaleFromUrl = !!props?.params?.locale; + const locale: string = hasLocaleFromUrl ? props?.params?.locale : DEFAULT_LOCALE; // If the locale isn't found (e.g: 404 page) + const lang: string = locale.split('-')?.[0]; + const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)]; + const gcmsLocales: string = prepareGraphCMSLocaleHeader(bestCountryCodes); + const i18nTranslations: I18nextResources = await fetchTranslations(lang); // Pre-fetches translations from Locize API + const apolloClient = createApolloClient(); + const variables = { + customerRef, + }; + const queryOptions = { + displayName: 'LAYOUT_QUERY', + query: LAYOUT_QUERY, + variables, + context: { + headers: { + 'gcms-locales': gcmsLocales, + }, + }, + }; + + const { + data, + errors, + loading, + networkStatus, + stale, + }: ApolloQueryResult<{ + customer: Customer; + }> = await apolloClient.query(queryOptions); + + if (errors) { + console.error(errors); + throw new Error('Errors were detected in GraphQL query.'); + } + + const { + customer, + } = data || {}; // XXX Use empty object as fallback, to avoid app crash when destructuring, if no data is returned + const dataset = { + customer, + }; + + return { + // Props returned here will be available as page properties (pageProps) + props: { + apolloState: apolloClient.cache.extract(), + bestCountryCodes, + serializedDataset: serializeSafe(dataset), + customerRef, + i18nTranslations, + gcmsLocales, + hasLocaleFromUrl, + isReadyToRender: true, + isStaticRendering: true, + lang, + locale, + preview, + previewData, + }, + // revalidate: false, + }; +}; + diff --git a/src/layouts/core/SSR.ts b/src/layouts/core/SSR.ts new file mode 100644 index 000000000..d887d5f6d --- /dev/null +++ b/src/layouts/core/SSR.ts @@ -0,0 +1,135 @@ +import { CommonServerSideParams } from '@/app/types/CommonServerSideParams'; +import { PublicHeaders } from '@/layouts/core/types/PublicHeaders'; +import { SSRPageProps } from '@/layouts/core/types/SSRPageProps'; +import { Cookies } from '@/modules/core/cookiesManager/types/Cookies'; +import UniversalCookiesManager from '@/modules/core/cookiesManager/UniversalCookiesManager'; +import { GenericObject } from '@/modules/core/data/types/GenericObject'; +import { prepareGraphCMSLocaleHeader } from '@/modules/core/gql/graphcms'; +import createApolloClient from '@/modules/core/gql/graphql'; +import { ApolloQueryOptions } from '@/modules/core/gql/types/ApolloQueryOptions'; +import { + DEFAULT_LOCALE, + resolveFallbackLanguage, + SUPPORTED_LANGUAGES, +} from '@/modules/core/i18n/i18n'; +import { + fetchTranslations, + I18nextResources, +} from '@/modules/core/i18n/i18nextLocize'; +import { isQuickPreviewRequest } from '@/modules/core/quickPreview/quickPreview'; +import { UserSemiPersistentSession } from '@/modules/core/userSession/types/UserSemiPersistentSession'; +import * as Sentry from '@sentry/node'; +import universalLanguageDetect from '@unly/universal-language-detector'; +import { ERROR_LEVELS } from '@unly/universal-language-detector/lib/utils/error'; +import { NormalizedCacheObject } from 'apollo-cache-inmemory'; +import { ApolloClient } from 'apollo-client'; +import { IncomingMessage } from 'http'; +import { + GetServerSideProps, + GetServerSidePropsContext, + GetServerSidePropsResult, +} from 'next'; +import NextCookies from 'next-cookies'; +import { LAYOUT_QUERY } from '../../gql/common/layoutQuery'; + +/** + * getDemoServerSideProps returns only part of the props expected in SSRPageProps + * To avoid TS issue, we omit those that we don't return, and add those necessary to the getServerSideProps function + */ +export type GetCoreServerSidePropsResults = Omit & { + apolloClient: ApolloClient; + layoutQueryOptions: ApolloQueryOptions; + headers: PublicHeaders; +} + +/** + * Only executed on the server side, for every request. + * Computes some dynamic props that should be available for all SSR pages that use getServerSideProps + * + * Because the exact GQL query will depend on the consumer (AKA "caller"), this helper doesn't run any query by itself, but rather return all necessary props to allow the consumer to perform its own queries + * This improves performances, by only running one GQL query instead of many (consumer's choice) + * + * Meant to avoid code duplication + * + * XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly. + * + * @see https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering + */ +export const getCoreServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext): Promise> => { + const { + query, + params, + req, + res, + } = context; + const isQuickPreviewPage: boolean = isQuickPreviewRequest(req); + const customerRef: string = process.env.NEXT_PUBLIC_CUSTOMER_REF; + const readonlyCookies: Cookies = NextCookies(context); // Parses Next.js cookies in a universal way (server + client) + const cookiesManager: UniversalCookiesManager = new UniversalCookiesManager(req, res); // Cannot be forwarded as pageProps, because contains circular refs + const userSession: UserSemiPersistentSession = cookiesManager.getUserData(); + const { headers }: IncomingMessage = req; + const publicHeaders: PublicHeaders = { + 'accept-language': headers?.['accept-language'], + 'user-agent': headers?.['user-agent'], + 'host': headers?.host, + }; + const hasLocaleFromUrl = !!query?.locale; + // Resolve locale from query, fallback to browser headers + const locale: string = hasLocaleFromUrl ? query?.locale as string : universalLanguageDetect({ + supportedLanguages: SUPPORTED_LANGUAGES, // Whitelist of supported languages, will be used to filter out languages that aren't supported + fallbackLanguage: DEFAULT_LOCALE, // Fallback language in case the user's language cannot be resolved + acceptLanguageHeader: req?.headers?.['accept-language'], // Optional - Accept-language header will be used when resolving the language on the server side + serverCookies: readonlyCookies, // Optional - Cookie "i18next" takes precedence over navigator configuration (ex: "i18next: fr"), will only be used on the server side + errorHandler: (error: Error, level: ERROR_LEVELS, origin: string, context: GenericObject): void => { + Sentry.withScope((scope): void => { + scope.setExtra('level', level); + scope.setExtra('origin', origin); + scope.setContext('context', context); + Sentry.captureException(error); + }); + // eslint-disable-next-line no-console + console.error(error.message); + }, + }); + const lang: string = locale.split('-')?.[0]; + const bestCountryCodes: string[] = [lang, resolveFallbackLanguage(lang)]; + const gcmsLocales: string = prepareGraphCMSLocaleHeader(bestCountryCodes); + const i18nTranslations: I18nextResources = await fetchTranslations(lang); // Pre-fetches translations from Locize API + const apolloClient = createApolloClient(); + const variables = { + customerRef, + }; + const layoutQueryOptions: ApolloQueryOptions = { + displayName: 'LAYOUT_QUERY', + query: LAYOUT_QUERY, + variables, + context: { + headers: { + 'gcms-locales': gcmsLocales, + }, + }, + }; + + // Most props returned here will be necessary for the app to work properly (see "SSRPageProps") + // Some props are meant to be helpful to the consumer and won't be passed down to the _app.render (e.g: apolloClient, layoutQueryOptions) + return { + props: { + apolloClient, + bestCountryCodes, + serializedDataset: null, // We don't send the dataset yet (we don't have any because we haven't fetched the database yet), but it must be done by SSR pages in"getServerSideProps" + customerRef, + i18nTranslations, + headers: publicHeaders, + gcmsLocales, + hasLocaleFromUrl, + isReadyToRender: true, + isServerRendering: true, + lang, + locale, + layoutQueryOptions, + readonlyCookies, + userSession, + isQuickPreviewPage, + }, + }; +}; diff --git a/src/components/pageLayouts/Footer.tsx b/src/layouts/core/components/Footer.tsx similarity index 86% rename from src/components/pageLayouts/Footer.tsx rename to src/layouts/core/components/Footer.tsx index 8f2f7b14b..76eeeea2a 100644 --- a/src/components/pageLayouts/Footer.tsx +++ b/src/layouts/core/components/Footer.tsx @@ -1,19 +1,19 @@ +import { NRN_CO_BRANDING_LOGO_URL } from '@/app/constants'; +import Logo from '@/components/dataDisplay/Logo'; +import { CSSStyles } from '@/modules/core/css/types/CSSStyles'; +import useCustomer from '@/modules/core/data/hooks/useCustomer'; +import { Asset } from '@/modules/core/data/types/Asset'; +import { Customer } from '@/modules/core/data/types/Customer'; +import GraphCMSAsset from '@/modules/core/gql/components/GraphCMSAsset'; +import I18nBtnChangeLocale from '@/modules/core/i18n/components/I18nBtnChangeLocale'; +import I18nLink from '@/modules/core/i18n/components/I18nLink'; +import { SIZE_XS } from '@/utils/logo'; import { css, useTheme, } from '@emotion/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { NRN_CO_BRANDING_LOGO_URL } from '../../constants'; -import useCustomer from '../../hooks/useCustomer'; -import { CSSStyles } from '../../types/CSSStyles'; -import { Asset } from '../../types/data/Asset'; -import { Customer } from '../../types/data/Customer'; -import { SIZE_XS } from '../../utils/assets/logo'; -import GraphCMSAsset from '../assets/GraphCMSAsset'; -import Logo from '../assets/Logo'; -import I18nBtnChangeLocale from '../i18n/I18nBtnChangeLocale'; -import I18nLink from '../i18n/I18nLink'; export type Props = { style?: CSSStyles; @@ -170,3 +170,4 @@ const Footer: React.FunctionComponent = (props) => { }; export default Footer; + diff --git a/src/components/pageLayouts/Head.tsx b/src/layouts/core/components/Head.tsx similarity index 90% rename from src/components/pageLayouts/Head.tsx rename to src/layouts/core/components/Head.tsx index 3599d9543..ce6e76e90 100644 --- a/src/components/pageLayouts/Head.tsx +++ b/src/layouts/core/components/Head.tsx @@ -1,13 +1,12 @@ import { isBrowser } from '@unly/utils'; import NextHead from 'next/head'; import React from 'react'; - import { NRN_DEFAULT_FONT, NRN_DEFAULT_SERVICE_LABEL, -} from '../../constants'; -import { I18nLocale } from '../../types/i18n/I18nLocale'; -import { SUPPORTED_LOCALES } from '../../utils/i18n/i18n'; +} from '@/app/constants'; +import { I18nLocale } from '@/modules/core/i18n/types/I18nLocale'; +import { SUPPORTED_LOCALES } from '@/modules/core/i18n/i18n'; export type HeadProps = { seoTitle?: string; @@ -19,7 +18,14 @@ export type HeadProps = { } /** - * Custom Head component + * Custom Next.js Head component. + * + * Configures SEO, load fonts. + * + * TODO Fonts should be loaded differently. Lee Robinson (Vercel) has given great talks recently, see https://leerob.io/blog/fonts + * TODO SEO should be done differently. See https://github.com/UnlyEd/next-right-now/issues/150 + * + * XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly. * * https://github.com/vercel/next.js#populating-head */ diff --git a/src/layouts/core/components/Layout.tsx b/src/layouts/core/components/Layout.tsx new file mode 100644 index 000000000..4e60275ae --- /dev/null +++ b/src/layouts/core/components/Layout.tsx @@ -0,0 +1,132 @@ +import { SoftPageProps } from '@/layouts/core/types/SoftPageProps'; +import { GenericObject } from '@/modules/core/data/types/GenericObject'; +import DefaultErrorLayout from '@/modules/core/errorHandling/DefaultErrorLayout'; +import PreviewModeBanner from '@/modules/core/previewMode/components/PreviewModeBanner'; +import Sentry from '@/modules/core/sentry/sentry'; +import ErrorPage from '@/pages/_error'; +import { + Amplitude, + LogOnMount, +} from '@amplitude/react-amplitude'; +import { createLogger } from '@unly/utils-simple-logger'; +import classnames from 'classnames'; +import { + NextRouter, + useRouter, +} from 'next/router'; +import React from 'react'; +import BaseFooter from './Footer'; +import Head, { HeadProps } from './Head'; +import Nav from './Nav'; +import DefaultPageContainer from './PageContainer'; + +const fileLabel = 'components/pageLayouts/DefaultLayout'; +const logger = createLogger({ + label: fileLabel, +}); + +type Props = { + children: React.ReactNode; + headProps?: HeadProps; + pageName: string; + PageContainer?: React.FunctionComponent; +} & SoftPageProps; + +/** + * Handles the positioning of top-level elements within the page. + * + * It does the following: + * - Adds a Nav/Footer component, and the dynamic Next.js "Page" component in between. + * - Automatically track page views (Amplitude). + * - Handles errors by displaying the Error page, with the ability to contact technical support (which will send a Sentry User Feedback). + * + * XXX Core component, meant to be used by other layouts, shouldn't be used by other components directly. + */ +const Layout: React.FunctionComponent = (props): JSX.Element => { + const { + children, + error, + isInIframe = false, // Won't be defined server-side + headProps = {}, + pageName, + PageContainer = DefaultPageContainer, + } = props; + const router: NextRouter = useRouter(); + const isIframeWithFullPagePreview = router?.query?.fullPagePreview === '1'; + + Sentry.addBreadcrumb({ // See https://docs.sentry.io/enriching-error-data/breadcrumbs + category: fileLabel, + message: `Rendering ${fileLabel} for page ${pageName}`, + level: Sentry.Severity.Debug, + }); + + Sentry.configureScope((scope): void => { + scope.setTag('fileLabel', fileLabel); + }); + + return ( + ({ + ...inheritedProps, + page: { + ...inheritedProps.page, + name: pageName, + }, + })} + > + + + + {/* Loaded from components/Head - See https://github.com/mikemaccana/outdated-browser-rework */} + {/**/} + + { + // XXX You may want to enable preview mode during non-production stages only + process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && ( + + ) + } + + { + (!isInIframe || isIframeWithFullPagePreview) && ( +