From 73f5119f3f6cc8fe4a6cf7f202251e4f5ca8d566 Mon Sep 17 00:00:00 2001 From: Vadorequest Date: Sun, 17 Jan 2021 00:56:45 +0100 Subject: [PATCH 1/8] Add StoryBook (#251) --- .../workflows/deploy-vercel-production.yml | 2 +- .github/workflows/deploy-vercel-staging.yml | 4 +- .github/workflows/deploy-vercel-storybook.yml | 249 ++++++++++++++ .gitignore | 3 +- .storybook/.gitignore | 1 + .storybook/babel.config.js | 13 + .storybook/main.js | 223 ++++++++++++ .storybook/manager.js | 36 ++ .storybook/mock/sb-dataset.js | 239 +++++++++++++ .storybook/preview-body.html | 122 +++++++ .storybook/preview-head.html | 6 + .storybook/preview.js | 272 +++++++++++++++ .storybook/webpack.config.js | 36 ++ babel.config.js | 2 +- cypress/config-customer-ci-cd.json | 2 +- cypress/config-storybook.json | 11 + .../app/_sanity/1-domain.ts | 22 ++ .../app/_sanity/README.md | 12 + .../app/stories/dataDisplay/btn.ts | 30 ++ cypress/support/commands.d.ts | 1 + cypress/support/commands.js | 22 ++ package.json | 27 ++ src/components/animations/Loader.tsx | 3 +- .../appBootstrap/BrowserPageBootstrap.tsx | 13 +- .../appBootstrap/MultiversalAppBootstrap.tsx | 14 +- .../MultiversalGlobalExternalStyles.tsx | 12 + .../appBootstrap/MultiversalGlobalStyles.tsx | 19 +- src/components/errors/DefaultErrorLayout.tsx | 7 +- src/components/errors/ErrorDebug.tsx | 2 + src/components/i18n/I18nBtnChangeLocale.tsx | 13 +- src/components/i18n/I18nLink.tsx | 80 ++++- src/components/pageLayouts/Footer.tsx | 10 +- src/components/pageLayouts/Nav.tsx | 43 ++- .../pageLayouts/PreviewModeBanner.tsx | 18 +- .../pageLayouts/QuickPreviewBanner.tsx | 15 +- .../rehydration/DisplayOnBrowserMount.tsx | 2 +- src/components/svg/Animated3Dots.tsx | 6 +- src/components/svg/AnimatedTextBubble.tsx | 7 +- src/components/svg/EnglishHybridFlag.tsx | 6 +- src/components/svg/EnglishUkFlag.tsx | 6 +- src/components/utils/Btn.tsx | 31 +- src/components/utils/BubbleTimer.tsx | 12 +- src/components/utils/Buttons.tsx | 233 ------------- src/components/utils/Cards.tsx | 21 +- src/components/utils/CircleBtn.tsx | 34 +- src/components/utils/Code.tsx | 29 +- src/components/utils/DocumentButton.tsx | 7 +- src/components/utils/EllipsisText.tsx | 49 ++- src/components/utils/ExternalLink.tsx | 65 +++- src/components/utils/LanguageSelector.tsx | 27 -- src/components/utils/LegalContent.tsx | 31 +- src/components/utils/LinkButton.tsx | 8 +- src/components/utils/Markdown.tsx | 34 +- src/components/utils/SimpleTooltip.tsx | 26 +- .../{SpoilerButton.tsx => SpoilerLink.tsx} | 48 ++- src/components/utils/Stamp.tsx | 25 +- src/components/utils/Text.tsx | 30 +- src/components/utils/ToggleButton.tsx | 59 +++- .../utils/ToggleLanguagesButton.tsx | 43 ++- src/components/utils/Tooltip.tsx | 45 ++- src/hocs/withHOCTemplate.tsx | 25 +- src/i18nConfig.js | 54 ++- src/pages/_app.tsx | 5 +- src/stories/app/README.stories.mdx | 49 +++ src/stories/nrn/Welcome.stories.mdx | 53 +++ src/stories/nrn/a11y/example.stories.tsx | 22 ++ .../nrn/animation/Animated3Dots.stories.tsx | 21 ++ .../animation/AnimatedTextBubble.stories.tsx | 21 ++ .../nrn/animation/BubbleTimer.stories.tsx | 28 ++ src/stories/nrn/animation/Loader.stories.tsx | 18 + .../nrn/asset/AirtableAsset.stories.tsx | 39 +++ src/stories/nrn/dataDisplay/Btn.stories.tsx | 194 +++++++++++ .../nrn/dataDisplay/CircleBtn.stories.tsx | 36 ++ src/stories/nrn/dataDisplay/Code.stories.tsx | 51 +++ .../dataDisplay/DocumentButton.stories.tsx | 35 ++ .../nrn/dataDisplay/ExternalLink.stories.tsx | 42 +++ .../nrn/dataDisplay/LinkButton.stories.tsx | 58 ++++ .../nrn/dataDisplay/Markdown.stories.tsx | 46 +++ .../nrn/dataDisplay/SpoilerLink.stories.tsx | 45 +++ src/stories/nrn/dataDisplay/Stamp.stories.tsx | 43 +++ src/stories/nrn/dataDisplay/Text.stories.tsx | 42 +++ .../nrn/dataDisplay/ToggleButton.stories.tsx | 68 ++++ .../ToggleLanguagesButton.stories.tsx | 34 ++ .../nrn/i18n/I18nBtnChangeLocale.stories.tsx | 25 ++ src/stories/nrn/i18n/I18nLink.module.css | 6 + src/stories/nrn/i18n/I18nLink.stories.tsx | 74 ++++ src/stories/nrn/layout/Cards.stories.tsx | 321 ++++++++++++++++++ .../nrn/layout/DefaultErrorLayout.stories.tsx | 36 ++ src/stories/nrn/layout/Footer.stories.tsx | 18 + .../nrn/layout/LegalContent.stories.tsx | 40 +++ src/stories/nrn/layout/Nav.stories.tsx | 18 + .../nrn/layout/PreviewModeBanner.stories.tsx | 18 + .../nrn/layout/QuickPreviewBanner.stories.tsx | 18 + .../nrn/layout/QuickPreviewLayout.stories.tsx | 31 ++ .../nrn/overlay/SimpleTooltip.stories.tsx | 43 +++ src/stories/nrn/overlay/Tooltip.stories.tsx | 48 +++ .../DisplayOnBrowserMount.stories.tsx | 21 ++ src/stories/nrn/text/EllipsisText.stories.tsx | 36 ++ src/stories/sb-examples/Button.stories.tsx | 38 +++ src/stories/sb-examples/Button.tsx | 48 +++ src/stories/sb-examples/Header.stories.tsx | 20 ++ src/stories/sb-examples/Header.tsx | 47 +++ .../sb-examples/Introduction.stories.mdx | 103 ++++++ .../Official Storybook.stories.mdx | 37 ++ src/stories/sb-examples/Page.stories.tsx | 23 ++ src/stories/sb-examples/Page.tsx | 67 ++++ src/stories/sb-examples/button.css | 30 ++ src/stories/sb-examples/header.css | 26 ++ src/stories/sb-examples/page.css | 69 ++++ src/stories/shared/assets/code-brackets.svg | 1 + src/stories/shared/assets/colors.svg | 1 + src/stories/shared/assets/comments.svg | 1 + src/stories/shared/assets/direction.svg | 1 + src/stories/shared/assets/flow.svg | 1 + src/stories/shared/assets/plugin.svg | 1 + src/stories/shared/assets/repo.svg | 1 + src/stories/shared/assets/stackalt.svg | 1 + src/stories/shared/hocs/withPropMock.tsx | 81 +++++ src/types/react/ReactLinkProps.ts | 14 + src/utils/analytics/amplitude.ts | 15 +- src/utils/i18n/i18nextLocize.ts | 4 +- src/utils/icons/font-awesome.ts | 13 +- src/utils/theming/themedComponentColors.ts | 28 +- vercel.storybook.json | 13 + 124 files changed, 4436 insertions(+), 498 deletions(-) create mode 100644 .github/workflows/deploy-vercel-storybook.yml create mode 100644 .storybook/.gitignore create mode 100644 .storybook/babel.config.js create mode 100644 .storybook/main.js create mode 100644 .storybook/manager.js create mode 100644 .storybook/mock/sb-dataset.js create mode 100644 .storybook/preview-body.html create mode 100644 .storybook/preview-head.html create mode 100644 .storybook/preview.js create mode 100644 .storybook/webpack.config.js create mode 100644 cypress/config-storybook.json create mode 100644 cypress/integration-storybook/app/_sanity/1-domain.ts create mode 100644 cypress/integration-storybook/app/_sanity/README.md create mode 100644 cypress/integration-storybook/app/stories/dataDisplay/btn.ts create mode 100644 src/components/appBootstrap/MultiversalGlobalExternalStyles.tsx delete mode 100644 src/components/utils/Buttons.tsx delete mode 100644 src/components/utils/LanguageSelector.tsx rename src/components/utils/{SpoilerButton.tsx => SpoilerLink.tsx} (54%) create mode 100644 src/stories/app/README.stories.mdx create mode 100644 src/stories/nrn/Welcome.stories.mdx create mode 100644 src/stories/nrn/a11y/example.stories.tsx create mode 100644 src/stories/nrn/animation/Animated3Dots.stories.tsx create mode 100644 src/stories/nrn/animation/AnimatedTextBubble.stories.tsx create mode 100644 src/stories/nrn/animation/BubbleTimer.stories.tsx create mode 100644 src/stories/nrn/animation/Loader.stories.tsx create mode 100644 src/stories/nrn/asset/AirtableAsset.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/Btn.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/CircleBtn.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/Code.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/DocumentButton.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/ExternalLink.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/LinkButton.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/Markdown.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/SpoilerLink.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/Stamp.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/Text.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/ToggleButton.stories.tsx create mode 100644 src/stories/nrn/dataDisplay/ToggleLanguagesButton.stories.tsx create mode 100644 src/stories/nrn/i18n/I18nBtnChangeLocale.stories.tsx create mode 100644 src/stories/nrn/i18n/I18nLink.module.css create mode 100644 src/stories/nrn/i18n/I18nLink.stories.tsx create mode 100644 src/stories/nrn/layout/Cards.stories.tsx create mode 100644 src/stories/nrn/layout/DefaultErrorLayout.stories.tsx create mode 100644 src/stories/nrn/layout/Footer.stories.tsx create mode 100644 src/stories/nrn/layout/LegalContent.stories.tsx create mode 100644 src/stories/nrn/layout/Nav.stories.tsx create mode 100644 src/stories/nrn/layout/PreviewModeBanner.stories.tsx create mode 100644 src/stories/nrn/layout/QuickPreviewBanner.stories.tsx create mode 100644 src/stories/nrn/layout/QuickPreviewLayout.stories.tsx create mode 100644 src/stories/nrn/overlay/SimpleTooltip.stories.tsx create mode 100644 src/stories/nrn/overlay/Tooltip.stories.tsx create mode 100644 src/stories/nrn/reactRehydration/DisplayOnBrowserMount.stories.tsx create mode 100644 src/stories/nrn/text/EllipsisText.stories.tsx create mode 100644 src/stories/sb-examples/Button.stories.tsx create mode 100644 src/stories/sb-examples/Button.tsx create mode 100644 src/stories/sb-examples/Header.stories.tsx create mode 100644 src/stories/sb-examples/Header.tsx create mode 100644 src/stories/sb-examples/Introduction.stories.mdx create mode 100644 src/stories/sb-examples/Official Storybook.stories.mdx create mode 100644 src/stories/sb-examples/Page.stories.tsx create mode 100644 src/stories/sb-examples/Page.tsx create mode 100644 src/stories/sb-examples/button.css create mode 100644 src/stories/sb-examples/header.css create mode 100644 src/stories/sb-examples/page.css create mode 100644 src/stories/shared/assets/code-brackets.svg create mode 100644 src/stories/shared/assets/colors.svg create mode 100644 src/stories/shared/assets/comments.svg create mode 100644 src/stories/shared/assets/direction.svg create mode 100644 src/stories/shared/assets/flow.svg create mode 100644 src/stories/shared/assets/plugin.svg create mode 100644 src/stories/shared/assets/repo.svg create mode 100644 src/stories/shared/assets/stackalt.svg create mode 100644 src/stories/shared/hocs/withPropMock.tsx create mode 100644 src/types/react/ReactLinkProps.ts create mode 100644 vercel.storybook.json diff --git a/.github/workflows/deploy-vercel-production.yml b/.github/workflows/deploy-vercel-production.yml index fadc2d24c..1dbb728db 100644 --- a/.github/workflows/deploy-vercel-production.yml +++ b/.github/workflows/deploy-vercel-production.yml @@ -21,7 +21,6 @@ # - See https://github.com/jwalton/gh-find-current-pr https://github.com/jwalton/gh-find-current-pr/tree/v1 # - See https://github.com/peter-evans/create-or-update-comment https://github.com/peter-evans/create-or-update-comment/tree/v1 # - See https://github.com/UnlyEd/github-action-await-vercel https://github.com/UnlyEd/github-action-await-vercel/tree/v1.1.1 -# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/v3.x # - See https://github.com/cypress-io/github-action https://github.com/cypress-io/github-action/tree/v2 # - See https://github.com/foo-software/lighthouse-check-action https://github.com/foo-software/lighthouse-check-action/tree/v1.0.1 @@ -71,6 +70,7 @@ jobs: # Workflow overview: # - Resolve customer to deploy from github event input (falls back to resolving it from vercel.json file) # - Deploy the customer in production + # - Creates multiple deployment aliases based on the "alias" property defined in the vercel.json file, and link them to the deployment # XXX You can use https://jqplay.org/ if you want to play around with "jq" to manipulate JSON run: | # Print the version of the "vercel" CLI being used (helps debugging) diff --git a/.github/workflows/deploy-vercel-staging.yml b/.github/workflows/deploy-vercel-staging.yml index 076b9d011..95aff8faf 100644 --- a/.github/workflows/deploy-vercel-staging.yml +++ b/.github/workflows/deploy-vercel-staging.yml @@ -22,7 +22,6 @@ # - See https://github.com/peter-evans/create-or-update-comment https://github.com/peter-evans/create-or-update-comment/tree/v1 # - See https://github.com/UnlyEd/github-action-await-vercel https://github.com/UnlyEd/github-action-await-vercel/tree/v1.1.1 # - See https://github.com/UnlyEd/github-action-store-variable https://github.com/UnlyEd/github-action-store-variable/tree/v1.0.1 -# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/v3.x # - See https://github.com/cypress-io/github-action https://github.com/cypress-io/github-action/tree/v2 # - See https://github.com/foo-software/lighthouse-check-action https://github.com/foo-software/lighthouse-check-action/tree/v1.0.1 @@ -75,6 +74,7 @@ jobs: # - Get stdout from deploy command (stderr prints build steps and stdout prints deployment url, which is what we are really looking for) # - Set the deployment url that will be included in the eventual PR comment # - Create a deployment alias based on the branch name, and link it to the deployment (so that each branch has its own domain automatically aliased to the latest commit) + # - Creates multiple deployment aliases based on the "alias" property defined in the vercel.json file, and link them to the deployment # XXX You can use https://jqplay.org/ if you want to play around with "jq" to manipulate JSON run: | # Print the version of the "vercel" CLI being used (helps debugging) @@ -372,7 +372,7 @@ jobs: with: # XXX We disabled "wait-on" option, because it's useless. Cypress will fail anyway, because it gets redirected to some internal Vercel URL if the domain isn't yet available - See https://github.com/cypress-io/github-action/issues/270 # wait-on: '${{ env.VERCEL_DEPLOYMENT_URL }}' # Be sure that the endpoint is ready by pinging it before starting tests, using a default timeout of 60 seconds - config-file: 'cypress/config-customer-ci-cd.json' # The config file itself doesn't matter because we will override most settings anyway. We just need `projectId` to run the tests. + config-file: 'cypress/config-customer-ci-cd.json' # Use Cypress config file for CI/CD, and override it below config: baseUrl=${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment env: # Enables Cypress debugging logs, very useful if Cypress crashes, like out-of-memory issues. diff --git a/.github/workflows/deploy-vercel-storybook.yml b/.github/workflows/deploy-vercel-storybook.yml new file mode 100644 index 000000000..9217af972 --- /dev/null +++ b/.github/workflows/deploy-vercel-storybook.yml @@ -0,0 +1,249 @@ +# Summary: +# Builds a static version of the Storybook website and triggers a new deployment on Vercel's platform, when anything is pushed in any branch. +# +# LEARN MORE AT https://unlyed.github.io/next-right-now/guides/ci-cd/ +# +# Dependencies overview: +# - See https://github.com/actions/setup-node https://github.com/actions/setup-node/tree/v1 +# - See https://github.com/actions/checkout https://github.com/actions/checkout/tree/v1 +# - See https://github.com/actions/upload-artifact https://github.com/actions/upload-artifact/tree/v1 +# - See https://github.com/rlespinasse/github-slug-action https://github.com/rlespinasse/github-slug-action/tree/3.x +# - See https://github.com/jwalton/gh-find-current-pr https://github.com/jwalton/gh-find-current-pr/tree/v1 +# - See https://github.com/peter-evans/create-or-update-comment https://github.com/peter-evans/create-or-update-comment/tree/v1 +# - See https://github.com/UnlyEd/github-action-await-vercel https://github.com/UnlyEd/github-action-await-vercel/tree/v1.1.1 +# - See https://github.com/UnlyEd/github-action-store-variable https://github.com/UnlyEd/github-action-store-variable/tree/v1.0.1 +# - See https://github.com/cypress-io/github-action https://github.com/cypress-io/github-action/tree/v2 + +name: Deploy Storybook static site to Vercel + +on: + # There are several ways to trigger Github actions - See https://help.github.com/en/actions/reference/events-that-trigger-workflows#example-using-a-single-event for a comprehensive list: + # - "push": Triggers each time a commit is pushed + # - "pull_request": Triggers each time a commit is pushed within a pull request, it makes it much easier to write comments within the PR, but it suffers some strong limitations: + # - There is no way to trigger when a PR is merged into another - See https://github.community/t/pull-request-action-does-not-run-on-merge/16092?u=vadorequest + # - It won't trigger when the PR is conflicting with its base branch - See https://github.community/t/run-actions-on-pull-requests-with-merge-conflicts/17104/2?u=vadorequest + push: # Triggers on each pushed commit + branches: + - '*' + +jobs: + # Configures the deployment environment, install dependencies (like node, npm, etc.) that are requirements for the upcoming jobs + # Ex: Necessary to run `yarn deploy` + setup-environment: + name: Setup deployment environment (Ubuntu 18.04 - Node 12.x) + runs-on: ubuntu-18.04 + steps: + - name: Installing node.js + uses: actions/setup-node@v1 # Used to install node environment - https://github.com/actions/setup-node + with: + node-version: '12.x' # Use the same node.js version as the one Vercel's uses (currently node12.x) + + # Starts a Vercel deployment, using the storybook configuration file + # N.B: It's Vercel that will perform the actual deployment + start-deployment: + name: Starts Vercel deployment (Ubuntu 18.04) + runs-on: ubuntu-18.04 + needs: setup-environment + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + - name: Expose GitHub slug/short variables # See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + uses: rlespinasse/github-slug-action@v3.x # See https://github.com/rlespinasse/github-slug-action + + - name: Deploying on Vercel + # Workflow overview: + # - Install yarn dependencies (necessary to build the Storybook static site) + # - Starts a Vercel deployment, using vercel.storybook.json + # - Creates multiple deployment aliases based on the "alias" property defined in the vercel.json file, and link them to the deployment + run: | + # Print the version of the "vercel" CLI being used (helps debugging) + vercel --version + echo "Current branch: ${GITHUB_REF_SLUG}" + + echo "Installing dependencies" + yarn install + + echo "Building and deploying StoryBook static site" + + # Deploy the customer on Vercel using the customer ref + # Store the output in a variable so we can extract metadata from it + VERCEL_DEPLOYMENT_OUTPUT=`yarn deploy:sb:gha --token $VERCEL_TOKEN` + + # Extract the Vercel deployment url from the deployment output + VERCEL_DEPLOYMENT_URL=`echo $VERCEL_DEPLOYMENT_OUTPUT | egrep -o 'https?://[^ ]+.vercel.app'` + echo "Deployment url: " $VERCEL_DEPLOYMENT_URL + echo "VERCEL_DEPLOYMENT_URL=$VERCEL_DEPLOYMENT_URL" >> $GITHUB_ENV + echo "VERCEL_DEPLOYMENT_DOMAIN=${VERCEL_DEPLOYMENT_URL#https://}" >> $GITHUB_ENV + + # Find all custom aliases configured in the customer deployment configuration file (vercel.json) + VERCEL_DEPLOYMENT_ALIASES_JSON=$(cat vercel.storybook.json | jq --raw-output '.alias') + echo "Custom aliases: " $VERCEL_DEPLOYMENT_ALIASES_JSON + + # Convert the JSON array into a bash array - See https://unix.stackexchange.com/a/615717/60329 + readarray -t VERCEL_DEPLOYMENT_ALIASES < <(jq --raw-output '.alias[]' < vercel.storybook.json) + + # Count the number of element in the array, will be 0 if it's an empty array, or if the "alias" key wasn't defined + VERCEL_DEPLOYMENT_ALIASES_COUNT=${#VERCEL_DEPLOYMENT_ALIASES[@]} + + # Check if there are no aliases configured + if [ "$VERCEL_DEPLOYMENT_ALIASES" > 0 ] + then + echo "$VERCEL_DEPLOYMENT_ALIASES_COUNT alias(es) found. Aliasing them now..." + + # For each alias configured, then assign it to the deployed domain + for DEPLOYMENT_ALIAS in "${VERCEL_DEPLOYMENT_ALIASES[@]}"; do + echo "npx vercel alias "$VERCEL_DEPLOYMENT_URL $DEPLOYMENT_ALIAS + npx vercel alias $VERCEL_DEPLOYMENT_URL $DEPLOYMENT_ALIAS --token $VERCEL_TOKEN || echo "Aliasing failed for '$DEPLOYMENT_ALIAS', but the build will continue regardless." + done + else + # $VERCEL_DEPLOYMENT_ALIASES is null, this happens when it was not defined in the vercel.json file + echo "There are no more aliases to configure. You can add more aliases from your vercel.json 'alias' property. See https://vercel.com/docs/configuration?query=alias%20domain#project/alias" + echo "$VERCEL_DEPLOYMENT_ALIASES" + fi + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} # Passing github's secret to the worker + # Passing exposed GitHub environment variables - See https://github.com/rlespinasse/github-slug-action#exposed-github-environment-variables + GITHUB_REF_SLUG: ${{ env.GITHUB_REF_SLUG }} + + # We need to find the PR id. Will be used later to comment on that PR. + - name: Finding Pull Request ID + uses: jwalton/gh-find-current-pr@v1 # See https://github.com/jwalton/gh-find-current-pr + id: pr_id_finder + if: always() # It forces the job to be always executed, even if a previous job fail. + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + # On deployment failure, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (Deployment failure) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: steps.pr_id_finder.outputs.number && failure() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ steps.pr_id_finder.outputs.number }} + body: | + :x:  Deployment **FAILED** + Commit ${{ github.sha }} failed to deploy **Storybook static site** to [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + [click to see logs](https://github.com/UnlyEd/next-right-now/pull/${{ steps.pr_id_finder.outputs.number }}/checks) + + # On deployment success, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (Deployment success) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: steps.pr_id_finder.outputs.number && success() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ steps.pr_id_finder.outputs.number }} + body: | + :white_check_mark:  Deployment **SUCCESS** + Commit ${{ github.sha }} successfully deployed **Storybook static site** to [${{ env.VERCEL_DEPLOYMENT_URL }}](${{ env.VERCEL_DEPLOYMENT_URL }}) + Deployment aliased as [nrn-v2-mst-aptd-at-lcz-sty-storybook](https://nrn-v2-mst-aptd-at-lcz-sty-storybook.vercel.app) + + # At the end of the job, store all variables we will need in the following jobs + # The variables will be stored in and retrieved from a GitHub Artifact (each variable is stored in a different file) + - name: Store variables for next jobs + uses: UnlyEd/github-action-store-variable@v1.0.1 # See https://github.com/UnlyEd/github-action-store-variable + with: + variables: | + VERCEL_DEPLOYMENT_URL=${{ env.VERCEL_DEPLOYMENT_URL }} + VERCEL_DEPLOYMENT_DOMAIN=${{ env.VERCEL_DEPLOYMENT_DOMAIN }} + GITHUB_PULL_REQUEST_ID=${{ steps.pr_id_finder.outputs.number }} + + # Waits for the Vercel deployment to reach "READY" state, so that other actions will be applied on a domain that is really online + await-for-vercel-deployment: + name: Await current deployment to be ready (Ubuntu 18.04) + runs-on: ubuntu-18.04 + needs: start-deployment + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v1.0.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + variables: | + VERCEL_DEPLOYMENT_DOMAIN + + # Wait for deployment to be ready, before running E2E (otherwise Cypress might start testing too early, and gets redirected to Vercel's "Login page", and tests fail) + - name: Awaiting Vercel deployment to be ready + uses: UnlyEd/github-action-await-vercel@v1.1.1 # See https://github.com/UnlyEd/github-action-await-vercel + id: await-vercel + env: + VERCEL_TOKEN: ${{ secrets.VERCEl_TOKEN }} + with: + deployment-url: ${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_DOMAIN }} # Must only contain the domain name (no http prefix, etc.) + timeout: 90 # Wait for 90 seconds before failing + + - name: Display deployment status + run: "echo The deployment is ${{ fromJson(steps.await-vercel.outputs.deploymentDetails).readyState }}" + + # Runs E2E tests against the Vercel deployment + run-2e2-tests: + name: Run end to end (E2E) tests (Ubuntu 18.04) + runs-on: ubuntu-18.04 + # Docker image with Cypress pre-installed + # https://github.com/cypress-io/cypress-docker-images/tree/master/included + container: cypress/included:3.8.3 + needs: await-for-vercel-deployment + steps: + - uses: actions/checkout@v1 # Get last commit pushed - See https://github.com/actions/checkout + + # Restore variables stored by previous jobs + - name: Restore variables + uses: UnlyEd/github-action-store-variable@v1.0.1 # See https://github.com/UnlyEd/github-action-store-variable + id: restore-variable + with: + variables: | + VERCEL_DEPLOYMENT_URL + GITHUB_PULL_REQUEST_ID + + # Runs the E2E tests against the new Vercel deployment + - name: Run E2E tests (Cypress) + uses: cypress-io/github-action@v2 # See https://github.com/cypress-io/github-action + with: + # XXX We disabled "wait-on" option, because it's useless. Cypress will fail anyway, because it gets redirected to some internal Vercel URL if the domain isn't yet available - See https://github.com/cypress-io/github-action/issues/270 + # wait-on: '${{ env.VERCEL_DEPLOYMENT_URL }}' # Be sure that the endpoint is ready by pinging it before starting tests, using a default timeout of 60 seconds + config-file: 'cypress/config-storybook.json' # Use Cypress config file for Storybook, and override it below + config: baseUrl=${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_URL }} # Overriding baseUrl provided by config file to test the new deployment + env: + # Enables Cypress debugging logs, very useful if Cypress crashes, like out-of-memory issues. + # DEBUG: "cypress:*" # Enable all logs. See https://docs.cypress.io/guides/references/troubleshooting.html#Print-DEBUG-logs + DEBUG: "cypress:server:util:process_profiler" # Enable logs for "memory and CPU usage". See https://docs.cypress.io/guides/references/troubleshooting.html#Log-memory-and-CPU-usage + + # On E2E failure, upload screenshots + - name: Upload screenshots artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: screenshots + path: cypress/screenshots/ + + # On E2E failure, upload videos + - name: Upload videos artifacts (E2E failure) + uses: actions/upload-artifact@v1 # On failure we upload artifacts, https://help.github.com/en/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts + if: failure() + with: + name: videos + path: cypress/videos/ + + # On E2E failure, add a comment to the PR with additional information, if there is an open PR for the current branch + - name: Comment PR (E2E failure) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: fromJson(steps.restore-variable.outputs.variables).GITHUB_PULL_REQUEST_ID && failure() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ fromJson(steps.restore-variable.outputs.variables).GITHUB_PULL_REQUEST_ID }} + body: | + :x:  E2E tests **FAILED** for commit ${{ github.sha }} previously deployed **Storybook static site** at [${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_URL }}](${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_URL }}) + Download artifacts (screenshots + videos) from [`checks`](https://github.com/UnlyEd/next-right-now/pull/${{ fromJson(steps.restore-variable.outputs.variables).GITHUB_PULL_REQUEST_ID }}/checks) section + + # On E2E success, add a comment to the PR, if there is an open PR for the current branch + - name: Comment PR (E2E success) + uses: peter-evans/create-or-update-comment@v1 # See https://github.com/peter-evans/create-or-update-comment + if: fromJson(steps.restore-variable.outputs.variables).GITHUB_PULL_REQUEST_ID && success() + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ fromJson(steps.restore-variable.outputs.variables).GITHUB_PULL_REQUEST_ID }} + body: | + :white_check_mark:  E2E tests **SUCCESS** for commit ${{ github.sha }} previously deployed **Storybook static site** at [${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_URL }}](${{ fromJson(steps.restore-variable.outputs.variables).VERCEL_DEPLOYMENT_URL }}) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index e58d8bedc..f943449c0 100644 --- a/.gitignore +++ b/.gitignore @@ -149,4 +149,5 @@ _site # Tmp files (cache, etc.) *.cache -.vercel +# Storybook +storybook-static/ diff --git a/.storybook/.gitignore b/.storybook/.gitignore new file mode 100644 index 000000000..f3e842096 --- /dev/null +++ b/.storybook/.gitignore @@ -0,0 +1 @@ +*.cache* diff --git a/.storybook/babel.config.js b/.storybook/babel.config.js new file mode 100644 index 000000000..ff1dbcd83 --- /dev/null +++ b/.storybook/babel.config.js @@ -0,0 +1,13 @@ +/** + * Babel configuration for Storybook + * + * Doesn't affect Next.js babel configuration, specific file for Storybook only. + * Need to apply Emotion babel configuration, otherwise Emotion "css" cannot be used in Storybook. + * + * XXX We use the "classic" way instead of the "automatic" way for Storybook, that's because MDX isn't compatible with "automatic". + * + * @see https://emotion.sh/docs/css-prop#babel-preset Configuring Emotion 11 + */ +module.exports = { + "presets": ["@emotion/babel-preset-css-prop"] +}; diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 000000000..2fcb96e75 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,223 @@ +const { promises: fs } = require('fs'); +const path = require('path'); +const fetch = require('node-fetch'); + +const toPath = (_path) => path.join(process.cwd(), _path); + +/** + * Fetches translations from Locize and store them in the filesystem. + * They will be loaded in preview.js, which will configure Locize so that components can display their translations. + * + * @param environment + */ +const fetchLocizeTranslation = async (environment) => { + const cacheFileName = '.sb-translations.cache.json'; + const version = environment === 'development' ? 'latest' : 'production'; + const languages = ['en', 'fr']; + const namespaces = ['common']; + const allI18nTranslations = {}; + + for (let i = 0; i < languages.length; i++) { + const lang = languages[i]; + for (let j = 0; j < namespaces.length; j++) { + const namespace = namespaces[j]; + const locizeAPIEndpoint = `https://api.locize.app/${process.env.NEXT_PUBLIC_LOCIZE_PROJECT_ID}/${version}/${lang}/${namespace}`; + console.log('Fetching translations from:', locizeAPIEndpoint); + const defaultI18nTranslationsResponse = await fetch(locizeAPIEndpoint); + const i18nTranslations = await defaultI18nTranslationsResponse.json(); + + allI18nTranslations[lang] = allI18nTranslations[lang] || {}; + allI18nTranslations[lang][namespace] = i18nTranslations; + } + } + + // Store translations + const translationCacheFile = path.join(__dirname, cacheFileName); + console.log('Writing translations cache to:', translationCacheFile); + + await fs.writeFile(translationCacheFile, JSON.stringify(allI18nTranslations, null, 2), 'utf8'); +}; + +module.exports = { + stories: [ + '../src/**/*.stories.mdx', + '../src/**/*.stories.@(js|jsx|ts|tsx)', + ], + addons: [ + /** + * The Storybook Links addon can be used to create links that navigate between stories in Storybook. + * + * @see https://www.npmjs.com/package/@storybook/addon-links + */ + '@storybook/addon-links', + + /** + * Present including "essential" Storybook addons, such as: + * + * - Actions - Storybook Addon Actions can be used to display data received by event handlers in Storybook. + * It's where the action you do are being logged. + * + * - Backgrounds - Storybook Addon Backgrounds can be used to change background colors inside the preview in Storybook. + * + * - Controls - Controls gives you a graphical UI to interact with a component's arguments dynamically, without needing to code. + * It creates an addon panel next to your component examples ("stories"), so you can edit them live. + * + * - Docs - Storybook Docs transforms your Storybook stories into world-class component documentation. + * - DocsPage: Out of the box, all your stories get a DocsPage. DocsPage is a zero-config aggregation + * of your component stories, text descriptions, docgen comments, props tables, and code examples into clean, readable pages. + * - MDX: If you want more control, MDX allows you to write long-form markdown documentation and stories in one file. + * You can also use it to write pure documentation pages and embed them inside your Storybook alongside your stories. + * + * - Viewport - Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in Storybook. + * This helps build responsive components inside of Storybook. + * + * - Toolbars - The Toolbars addon controls global story rendering options from Storybook's toolbar UI. It's a general purpose addon that can be used to: + * - set a theme for your components + * - set your components' internationalization (i18n) locale + * - configure just about anything in Storybook that makes use of a global variable + * + * @see https://storybook.js.org/addons/essentials + * @see https://github.com/storybookjs/storybook/tree/master/addons/essentials + * @see https://github.com/storybookjs/storybook/tree/next/addons/actions + * @see https://github.com/storybookjs/storybook/tree/next/addons/backgrounds + * @see https://github.com/storybookjs/storybook/tree/next/addons/controls + * @see https://github.com/storybookjs/storybook/tree/next/addons/docs + * @see https://github.com/storybookjs/storybook/tree/next/addons/viewport + * @see https://github.com/storybookjs/storybook/tree/next/addons/toolbars + * + * You can disable addons you don't want through configuration. + * @see https://github.com/storybookjs/storybook/tree/master/addons/essentials#disabling-addons + */ + { + name: '@storybook/addon-essentials', + options: { + actions: true, + backgrounds: true, + controls: true, + docs: true, + viewport: true, + toolbars: true, + }, + }, + + /** + * Storybook Addon Knobs has been replaced by Controls and is being deprecated, it will be removed in v7. + * + * It is listed below for documentation purpose and help you avoid using it thinking it's still legit. + * + * @see https://github.com/storybookjs/storybook/blob/next/addons/controls/README.md#how-will-this-replace-addon-knobs + */ + // '@storybook/addon-knobs', + + /** + * We use Google Analytics for tracking analytics usage. + * + * It's much easier to setup than Amplitude, because there is an official dedicated plugin for this. + * See ".storybook/manager.js" for Google Analytics configuration. + * + * @see https://github.com/storybookjs/storybook/tree/master/addons/google-analytics + */ + '@storybook/addon-google-analytics', + + /** + * Shows stories source in the addon panel. (display the source code of the story in a dedicated panel) + * + * Adds an "Story" tab. + * + * XXX Disabled for now, because of https://github.com/storybookjs/storybook/issues/13657 (brings no useful information at the moment) + * Better to use the Docs panel and display source code, that's a good workaround for now. + * + * @see https://github.com/storybookjs/storybook/tree/master/addons/storysource + */ + // '@storybook/addon-storysource', + + /** + * This storybook addon can be helpful to make your UI components more accessible. + * + * Adds an "Accessibility" tab. + * + * @see https://www.npmjs.com/package/@storybook/addon-a11y + */ + '@storybook/addon-a11y', + + // ------------------- Non official addons below ------------------ + + /** + * Adds support for CSS Modules. + * + * Even though Next Right Now doesn't encourage the use of CSS Modules, + * we thought it's an interesting feature to support, which is natively supported by Next.js. + * + * @see https://www.npmjs.com/package/storybook-css-modules-preset How to configure Storybook to support CSS Modules + * @see https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css How to use CSS Modules with Next.js + */ + 'storybook-css-modules-preset', + + /** + * A storybook addon to help better understand and debug performance for React components. + * + * Adds a "Performance" tab. + * + * @see https://github.com/atlassian-labs/storybook-addon-performance + */ + 'storybook-addon-performance/register', + + /** + * Offers suggestions on how you can improve the HTML, CSS and UX of your components to be more mobile-friendly. + * + * Adds a "Mobile" tab. + * + * @see https://github.com/aholachek/storybook-mobile + */ + 'storybook-mobile', + + /** + * A Storybook addon that embed Figma, websites, PDF or images in the addon panel. + * + * Adds a "Design" tab. + * + * @see https://github.com/pocka/storybook-addon-designs + */ + 'storybook-addon-designs', + ], + + /** + * Customize webpack configuration for Storybook. + * + * This doesn't affect the Next.js application, only the Storybook compilation. + * + * @param config + * @see https://storybook.js.org/docs/react/configure/overview#configure-your-storybook-project + */ + webpackFinal: async (config) => { + const { + mode: environment, + plugins, + module, + } = config; + await fetchLocizeTranslation(environment); + + return { + ...config, + resolve: { + ...config.resolve, + alias: { + ...config.resolve.alias, + + /** + * Map Emotion 10 libraries to Emotion 11 libraries. + * + * Otherwise Storybook fails to compile with "Module not found: Error: Can't resolve '@emotion/styled/base'", etc. + * It wasn't necessary to do this until we imported React component using "@emotion/styled". + * This issue is probably caused because Storybook uses Emotion 10 while we have Emotion 11 used by the Next.js app. + * + * @see https://github.com/storybookjs/storybook/issues/13277#issuecomment-751747964 + */ + '@emotion/core': toPath('node_modules/@emotion/react'), + '@emotion/styled': toPath('node_modules/@emotion/styled'), + 'emotion-theming': toPath('node_modules/@emotion/react'), + }, + }, + }; + }, +}; diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 000000000..2df85ffaa --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,36 @@ +import { addons } from '@storybook/addons'; +import { themes } from '@storybook/theming'; + +/** + * Configure Storybook UI layout. + * + * XXX The Storybook manager seems to suffer from a cache invalidation issue, which forces us to run with `--no-manager-cache` option. + * @see https://github.com/storybookjs/storybook/issues/13649#issuecomment-761076960 + * @see https://github.com/storybookjs/storybook/issues/13200 + * + * @see https://storybook.js.org/docs/react/configure/features-and-behavior + */ +addons.setConfig({ + theme: themes.dark, +}); + +/** + * Your Google Analytics tracking ID. + * + * If you're creating a dedicated Google Analytics property for this (you should), + * Then make sure to create a "Universal Analytics property", not a Google Analytics 4 property (default since 2021). + * + * @see https://support.google.com/analytics/answer/10269537 + * @see https://github.com/storybookjs/storybook/tree/master/addons/google-analytics Google Analytic addon for Storybook + */ +window.STORYBOOK_GA_ID = 'UA-89785688-10'; // Replace by your own "UA-XXXXXXX-XX" + +/** + * React-ga options object + * + * @example { debug: true, gaOptions: { userId: 123 }} + * @see https://github.com/react-ga/react-ga#api + * @see https://github.com/storybookjs/storybook/blob/4f5ab9fe9e590da7b841ec37cb1bed8d6327ea4b/addons/google-analytics/src/register.ts#L8 + * @see https://github.com/storybookjs/storybook/tree/master/addons/google-analytics Google Analytic addon for Storybook + */ +window.STORYBOOK_REACT_GA_OPTIONS = {}; diff --git a/.storybook/mock/sb-dataset.js b/.storybook/mock/sb-dataset.js new file mode 100644 index 000000000..826f344b2 --- /dev/null +++ b/.storybook/mock/sb-dataset.js @@ -0,0 +1,239 @@ +/** + * Dataset used by Storybook stories. + * + * Copied from a NRN instance "window.__CYPRESS_DATA__" and pasted there. + */ +const dataset = { + 'reci9HYsoqd1xScsi': { + '__typename': 'Customer', + 'id': 'reci9HYsoqd1xScsi', + 'ref': 'customer1', + 'label': 'Client 1', + 'availableLanguages': ['en', 'fr'], + 'theme': { + '__typename': 'Theme', + 'id': 'recrcZANU6L73OA9v', + 'primaryColor': '#00536F', + 'primaryColorVariant1': null, + 'onPrimaryColor': null, + 'secondaryColor': '#C90016', + 'secondaryColorVariant1': null, + 'onSecondaryColor': null, + 'backgroundColor': null, + 'onBackgroundColor': null, + 'surfaceColor': null, + 'onSurfaceColor': null, + 'errorColor': null, + 'onErrorColor': null, + 'fonts': null, + 'logo': { + 'id': 'attlGNQqFXvhDYOrR', + 'url': 'https://dl.airtable.com/.attachments/a16bd38af1f3fea3f894dd2a37dbf4bd/baa538c3/apple-touch-icon.png', + 'filename': 'apple-touch-icon.png', + 'size': 11769, + 'type': 'image/png', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/c8528519fa364ebc6c01a35834a06975/1720e171', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/f0f1a95475af253ef157f36faf598c99/2da93bc7', + 'width': 180, + 'height': 180, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/26551c4457369ea157cab82a2ac24368/8ea162cf', + 'width': 3000, + 'height': 3000, + }, + }, + }, + 'logoTitle': 'Awesome-looking Logo', + }, + 'products': [ + { + '__typename': 'Product', + 'id': 'recFSrY2znI6Z8Dbj', + 'ref': 'hellur', + 'title': 'Hellur', + 'images': [ + { + 'id': 'att6JU52f5PlMuiRu', + 'url': 'https://dl.airtable.com/Uvg7ldEEQpqKhR3NKTGt_348s.jpg', + 'filename': '348s.jpg', + 'size': 17866, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/8C4cVNCES89lt6PnFH5W_348s.jpg', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/TdSPVnVQISc0P0EdiiQw_348s.jpg', + 'width': 256, + 'height': 256, + }, + }, + }, + ], + 'imagesTitle': ['Big City'], + 'description': 'Super longue **description**\n\nVous pouvez même [utiliser des liens](https://bluebottlecoffee.com/releases/costa-rica-vista-al-valle-honey)\n', + 'price': 25, + 'status': 'DRAFT', + }, + { + '__typename': 'Product', + 'id': 'recXxSwjiehedMFPf', + 'ref': 'wow', + 'title': 'wow', + 'images': [ + { + 'id': 'attIQzHRvFMgmdytF', + 'url': 'https://dl.airtable.com/.attachments/00f7560832b1500d06d169233424ccd0/c5d69a9e/lXL1TfOBTiikTCW8DPT2', + 'filename': 'lXL1TfOBTiikTCW8DPT2', + 'size': 64273, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/8e2ab049c04e918ccf068eeb2363a24d/88511a89', + 'width': 54, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/6897834d90716b4221e88de8a7e5d17b/beacabda', + 'width': 729, + 'height': 486, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/d10c2858cbdd091db8ea5eb283d15ff5/8a483a20', + 'width': 3000, + 'height': 3000, + }, + }, + }, + ], + 'imagesTitle': ['wow'], + 'description': 'w\n', + 'price': null, + 'status': 'PUBLISHED', + }, + ], + 'serviceLabel': 'NRN demo 1', + 'termsDescription': '\nWe use this "terms" page to showcase what\'s achievable using Markdown + HTML + JSX.
\nYou can edit this through Stacker, see "Go to CMS" nav link.\n
\n\n---\n\nUsing Markdown\n\nHeading 2\nHeading 3\n\nBold\n\nItalic\n\nStrikethrough\n\nLink in new tab\n\n---\n\n

Using HTML

\n\n

Heading 4

\n
Heading 5
\n
Heading 6
\n\n
Text in div
\n\nBold\n\n
\n\nItalic\n\n
\n\nLink (same tab)\n\n
\n\nLink (new tab)\n\n---\n\nUsing JSX (React components)\n\nNote: Only a small subset of components are made available. It works based on a whitelist. You can see the full list here.\n\nComponents from Reactstrap\n\nAn Alert "info"\n\nAn Alert "success"\n\n\n\n
\n
\n\n\n\n
\n
\n\n\n\nCol 1 in a Row (with custom CSS)\n\n\nCol 2 in a Row\n\n\n\nCustom components\n\nHelp tooltips, using Tooltip component:
\nSome complex stuff (click/hover me!)\n\n
\n\nLocalised links, using I18nLink component:
\nLink to homepage, keeping current locale\n\n
\n\nButton to change the current locale, using I18nBtnChangeLocale component:
\n\n\n\n---\n\nNote: All links always open in a new tab with "noopener" to ensure proper security defaults. This only work when used from the app (not from Stacker)\n\nNote: Stacker can preview Markdown but not HTML/JSX. The behaviour between Stacker Markdown preview and real rendering can be different.\n\n\nAs you can see above, using JSX brings quite a few interesting capabilities. But it isn\'t all-powerful though.
\n\nIt\'s not possible to use JavaScript, so forget about using an onClick event for instance. You\'ll need to find workarounds for this kinds of things.
\n\nAlso, it\'s not possible to provide non-scalar props. Forget about providing a component with an object, or array, for instance.
\n\nNevertheless, it brings quite a few possibilities to your app. The secret is to keep things simple, using simple JSX components that rely on few props. The I18nBtnChangeLocale is a great example of that. No props, but changes the language for the whole app anyway, it\'s quite a powerful integration and very simple to use.\n
\n\n', + 'privacyDescription': '{serviceLabel} doesn\'t track any of your personal data.\n\nAnalytic data (such as page views) are being tracked for the whole site, **anonymously**.', + }, + 'recFSrY2znI6Z8Dbj': { + '__typename': 'Product', + 'id': 'recFSrY2znI6Z8Dbj', + 'ref': 'hellur', + 'title': 'Hellur', + 'images': [ + { + 'id': 'att6JU52f5PlMuiRu', + 'url': 'https://dl.airtable.com/Uvg7ldEEQpqKhR3NKTGt_348s.jpg', + 'filename': '348s.jpg', + 'size': 17866, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/8C4cVNCES89lt6PnFH5W_348s.jpg', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/TdSPVnVQISc0P0EdiiQw_348s.jpg', + 'width': 256, + 'height': 256, + }, + }, + }, + ], + 'imagesTitle': ['Big City'], + 'description': 'Super longue **description**\n\nVous pouvez même [utiliser des liens](https://bluebottlecoffee.com/releases/costa-rica-vista-al-valle-honey)\n', + 'price': 25, + 'status': 'DRAFT', + }, + 'recXxSwjiehedMFPf': { + '__typename': 'Product', + 'id': 'recXxSwjiehedMFPf', + 'ref': 'wow', + 'title': 'wow', + 'images': [ + { + 'id': 'attIQzHRvFMgmdytF', + 'url': 'https://dl.airtable.com/.attachments/00f7560832b1500d06d169233424ccd0/c5d69a9e/lXL1TfOBTiikTCW8DPT2', + 'filename': 'lXL1TfOBTiikTCW8DPT2', + 'size': 64273, + 'type': 'image/jpeg', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/8e2ab049c04e918ccf068eeb2363a24d/88511a89', + 'width': 54, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/6897834d90716b4221e88de8a7e5d17b/beacabda', + 'width': 729, + 'height': 486, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/d10c2858cbdd091db8ea5eb283d15ff5/8a483a20', + 'width': 3000, + 'height': 3000, + }, + }, + }, + ], + 'imagesTitle': ['wow'], + 'description': 'w\n', + 'price': null, + 'status': 'PUBLISHED', + }, + 'recrcZANU6L73OA9v': { + '__typename': 'Theme', + 'id': 'recrcZANU6L73OA9v', + 'primaryColor': 'black', + 'primaryColorVariant1': null, + 'onPrimaryColor': null, + 'secondaryColor': null, + 'secondaryColorVariant1': null, + 'onSecondaryColor': null, + 'backgroundColor': null, + 'onBackgroundColor': null, + 'surfaceColor': null, + 'onSurfaceColor': null, + 'errorColor': null, + 'onErrorColor': null, + 'fonts': null, + 'logo': { + 'id': 'attlGNQqFXvhDYOrR', + 'url': 'https://dl.airtable.com/.attachments/a16bd38af1f3fea3f894dd2a37dbf4bd/baa538c3/apple-touch-icon.png', + 'filename': 'apple-touch-icon.png', + 'size': 11769, + 'type': 'image/png', + 'thumbnails': { + 'small': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/c8528519fa364ebc6c01a35834a06975/1720e171', + 'width': 36, + 'height': 36, + }, + 'large': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/f0f1a95475af253ef157f36faf598c99/2da93bc7', + 'width': 180, + 'height': 180, + }, + 'full': { + 'url': 'https://dl.airtable.com/.attachmentThumbnails/26551c4457369ea157cab82a2ac24368/8ea162cf', + 'width': 3000, + 'height': 3000, + }, + }, + }, + 'logoTitle': 'Awesome-looking Logo', + }, +}; + +export default dataset; diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html new file mode 100644 index 000000000..dbafc706d --- /dev/null +++ b/.storybook/preview-body.html @@ -0,0 +1,122 @@ + + + diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 000000000..8dafd5621 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,6 @@ + + diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 000000000..55e4cb092 --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,272 @@ +import { Amplitude, AmplitudeProvider } from '@amplitude/react-amplitude'; +import { ThemeProvider } from '@emotion/react'; +import '@storybook/addon-console'; // Automatically forwards all logs in the "Actions" panel - See https://github.com/storybookjs/storybook-addon-console +import { addDecorator } from '@storybook/react'; +import { themes } from '@storybook/theming'; +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 dataset from './mock/sb-dataset'; + +// Loads translations from local file cache (Locize) +const i18nTranslations = require('./.sb-translations.cache.json'); + +/** + * Story Global parameters for Storybook. + * + * Parameters are a set of static, named metadata about a story, typically used to control the behavior of Storybook features and addons. + * Parameters are applied at the top-level and act as default values. + * + * XXX They can be overridden per component and per story. + * See https://storybook.js.org/docs/react/writing-stories/parameters#rules-of-parameter-inheritance + * + * @see https://storybook.js.org/docs/react/writing-stories/parameters Parameters documentation + * @see https://github.com/storybookjs/storybook/blob/master/addons/actions/ADVANCED.md#configuration + * @see https://storybook.js.org/docs/react/essentials/backgrounds#configuration + * + * Theme: + * Configure Storybook theme, using dark by default. + * You can customise this behavior per story using parameters. + * Configuring the theme in "manager.js" didn't work out. + * Also, the "Docs" section is better using the "normal" theme, for readability. + * + * @see https://storybook.js.org/docs/react/configure/theming#global-theming Global theming + * @see https://storybook.js.org/docs/react/configure/theming#theming-docs Per story theming (parameter) + * @see https://storybook.js.org/docs/react/configure/theming#create-a-theme-quickstart Creating your own theme + */ +export const parameters = { + actions: { + argTypesRegex: '^on[A-Z].*', + + /** + * Since Controls is built on the same engine as Storybook Docs, it can also show property documentation alongside your controls using the expanded parameter (defaults to false). + * We enable this for all stories by default. + * + * @see https://storybook.js.org/docs/react/essentials/controls#show-full-documentation-for-each-property + */ + expanded: true, + }, + + /** + * Configure stories argTypes for all stories. + * + * @deprecated Should not be used at the moment. See https://github.com/storybookjs/storybook/issues/11697 + * @see https://storybook.js.org/docs/react/essentials/controls + */ + // argTypes: {}, + + /** + * Options. + * Couldn't find centralized documentation about it. + */ + options: { + /** + * @see https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy#sorting-stories + */ + storySort: { + method: 'alphabetical', + order: [ + 'App', // Should be first + 'Next Right Now', // Should be second, if kept around + 'Storybook Examples', // Should be last, if kept around + ], + }, + }, + docs: { + theme: themes.normal, + }, +}; + +/** + * Storybook ships with toolbar items to control the viewport and background the story renders in. + * + * Below, we extend the native toolbar to add a few more options, such as i18n. + * Those global types can then be used in decorators, for both global decorators and story decorators. + * + * @description toolbar.item Can be either an array of plain strings, or a MenuItem. + * See https://storybook.js.org/docs/react/essentials/toolbars-and-globals#advanced-usage + * + * @description toolbar.icon The icon the will be displayed in the top toolbar. + * See https://www.chromatic.com/component?appId=5a375b97f4b14f0020b0cda3&name=Basics%7CIcon&mode=interactive&buildNumber=13899 + * + * @see https://storybook.js.org/docs/react/essentials/toolbars-and-globals + */ +export const globalTypes = { + locale: { + name: 'Locale', + description: 'Global locale for components', + defaultValue: defaultLocale, + toolbar: { + icon: 'globe', // See https://www.chromatic.com/component?appId=5a375b97f4b14f0020b0cda3&name=Basics%7CIcon&mode=interactive&buildNumber=13899 + items: supportedLocales.map(locale => locale.name), + }, + }, +}; + +/** + * Allow to use Next.js Router in Storybook stories. + * + * If you need to customise a component/story, then you should see https://github.com/lifeiscontent/storybook-addon-next-router#as-a-decorator-in-a-story + * You'll need to specify the Router behavior per-story if the below default config doesn't suit you. + * + * @see https://github.com/lifeiscontent/storybook-addon-next-router#usage-in-previewjs + */ +addDecorator( + withNextRouter({ + path: '/', // defaults to `/` + asPath: '/', // defaults to `/` + query: {}, // defaults to `{}` + // @formatter:off Disables odd WebStorm formatting for next line + push() {}, // defaults to using addon actions integration, can override any method in the router + // @formatter:on + }), +); + +/** + * Decorators in .storybook/preview.js are useful to mock Stories. + * + * Like parameters, decorators can be defined globally, at the component level and for a single story (as we’ve seen). + * All decorators, defined at all levels that apply to a story will run whenever that story is rendered, in the order: + * - Global decorators, in the order they are defined + * - Component decorators, in the order they are defined + * - Story decorators, in the order they are defined. + * + * @see https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking + * @see https://storybook.js.org/docs/react/writing-stories/decorators#global-decorators + */ +export const decorators = [ + /** + * Mock variables used to initialize all stories. + * + * Mocking those ensures the components relying on them will work as expected. + * Basically, plays a similar role to _app and appBootstrap components (MultiversalAppBootstrap, etc.) + * + * About Amplitude analytics: + * - We don't want to track analytics using Amplitude. + * - All analytics is disabled when running a component through Storybook preview. + * + * About Google analytics, see ".storybook/main.js" documentation. + * + * @see https://storybook.js.org/docs/react/essentials/toolbars-and-globals#create-a-decorator Context and globals + */ + (Story, context) => { + // console.log('context', context) // Prints useful information about the Story's configuration + // Configure i18n. In Storybook, the locale can be set from the top Toolbar. + const locale = context?.globals?.locale || defaultLocale; + const lang = getLangFromLocale(locale); + + // Applies i18next configuration with Locize backend + // Extra features like saveMissing, etc. will be disabled in production because Storybook doesn't have access to NEXT_PUBLIC_* environment variables there + // Although, they are configured in the same way as the Next.js app during development mode + i18nextLocize(lang, i18nTranslations); + + const customer = find(dataset, { __typename: 'Customer' }); + const customerTheme = initCustomerTheme(customer); + // console.log('customer', customer) + // console.log('customerTheme', customerTheme) + const customerRef = 'storybook'; // Fake customer ref + const amplitudeApiKey = ''; // Use invalid amplitude tracking key to force disable all amplitude analytics + const userConsent = { + isUserOptedOutOfAnalytics: true, // Disables all amplitude analytics tracking (even if a proper api key was being used) + hasUserGivenAnyCookieConsent: false, + }; + const userId = 'storybook'; // Fake id (would avoid user tracking even if correct api key was being used) + const amplitudeInstance = getAmplitudeInstance({ + customerRef, + iframeReferrer: null, + isInIframe: false, + lang, + locale, + userId, + userConsent: userConsent, + }); + + // Configure all providers, similarly to what being done by MultiversalAppBootstrap and BrowserPageBootstrap + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +]; + +/** + * Enables storybook-addon-performance for all stories by default. + * + * @see https://github.com/atlassian-labs/storybook-addon-performance#installation + */ +addDecorator(withPerformance); diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js new file mode 100644 index 000000000..ec60ae64b --- /dev/null +++ b/.storybook/webpack.config.js @@ -0,0 +1,36 @@ +/** + * The doc doesn't really mention using webpack.config.js, but .storybook/main.js instead. + * + * Nevertheless, configuring the webpack.config.js seems to work fine. + * + * @param config + * @param mode + * @return {Promise<*>} + * @see https://storybook.js.org/docs/react/configure/webpack + * @see https://storybook.js.org/docs/react/configure/webpack#using-your-existing-config + */ +module.exports = async ({ + config, + mode, +}) => { + /** + * Fixes npm packages that depend on `fs` module, etc. + * + * E.g: "winston" would fail to load without this, because it relies on fs, which isn't available during browser build. + * + * @see https://github.com/storybookjs/storybook/issues/4082#issuecomment-495370896 + */ + config.node = { + fs: 'empty', + tls: 'empty', + net: 'empty', + module: 'empty', + console: true, + }; + + // XXX See https://github.com/vercel/next.js/blob/canary/examples/with-sentry-simple/next.config.js + // Because StoryBook only compiles for client and has no server runtime, we must replace backend-related libs like @sentry/node to their browser counterpart + config.resolve.alias['@sentry/node'] = '@sentry/browser'; + + return config; +}; diff --git a/babel.config.js b/babel.config.js index 507d27a27..12d89e60c 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,7 +5,7 @@ * * @see https://nextjs.org/docs/advanced-features/customizing-babel-config Official doc reference v10 * @see https://github.com/vercel/next.js/blob/canary/packages/next/build/babel/preset.ts You can take a look at this file to learn about the presets included by next/babel. - * @see https://emotion.sh/docs/css-prop##babel-preset Configuring Emotion 11 + * @see https://emotion.sh/docs/css-prop#babel-preset Configuring Emotion 11 * @example https://github.com/vercel/next.js/tree/canary/examples/with-custom-babel-config Next.js official example of customizing Babel */ module.exports = { diff --git a/cypress/config-customer-ci-cd.json b/cypress/config-customer-ci-cd.json index 498e46bcf..fbd83be3e 100644 --- a/cypress/config-customer-ci-cd.json +++ b/cypress/config-customer-ci-cd.json @@ -1,5 +1,5 @@ { - "//": "This file is used by CI/CD GitHub Actions (staging + production) and not meant to be used locally", + "//": "This file is used by CI/CD GitHub Actions (deploy-vercel-staging/production) and not meant to be used locally", "baseUrl": "https://nrn-customer.vercel.app", "projectId": "4dvdog", "screenshotsFolder": "cypress/screenshots/customer", diff --git a/cypress/config-storybook.json b/cypress/config-storybook.json new file mode 100644 index 000000000..8d2cebb89 --- /dev/null +++ b/cypress/config-storybook.json @@ -0,0 +1,11 @@ +{ + "//": "This file is used by CI/CD GitHub Actions (deploy-vercel-storybook) and not meant to be used locally", + "baseUrl": "http://localhost:6006", + "projectId": "4dvdog", + "screenshotsFolder": "cypress/screenshots/storybook", + "videosFolder": "cypress/videos/storybook", + "integrationFolder": "cypress/integration-storybook", + "env": {}, + "ignoreTestFiles": "*.md", + "experimentalComponentTesting": true +} diff --git a/cypress/integration-storybook/app/_sanity/1-domain.ts b/cypress/integration-storybook/app/_sanity/1-domain.ts new file mode 100644 index 000000000..51947c49b --- /dev/null +++ b/cypress/integration-storybook/app/_sanity/1-domain.ts @@ -0,0 +1,22 @@ +const baseUrl = Cypress.config().baseUrl; + +describe('Sanity checks > Domain', () => { + /* + * Visits the home page before any test + */ + before(() => { + cy.visit('/'); + }); + + it(`should be running on the domain "${baseUrl}"`, () => { + cy.url().then((url) => { + cy.log(`Expected to be running on:`); + cy.log(baseUrl); + cy.log(`Actually running at:`); + cy.log(url); + cy.url({ timeout: 300000 }).should('contains', baseUrl); // Wait at least 5 minute before timing out + }); + }); +}); + +export {}; diff --git a/cypress/integration-storybook/app/_sanity/README.md b/cypress/integration-storybook/app/_sanity/README.md new file mode 100644 index 000000000..77784d7b8 --- /dev/null +++ b/cypress/integration-storybook/app/_sanity/README.md @@ -0,0 +1,12 @@ +Sanity checks +=== + +The purpose of this folder is to contain sanity tests that should be executed before the others. +We use it to make sure we're on the right domain before running our tests, because it can happen E2E test start before Vercel if fully configured, and thus the tests would run on the wrong page. +To avoid this to happen, we first wait up to 5mn before timing out when checking the domain name. This ensures other tests don't run on an unexpected domain and fail, which would be misleading. + +This way, the first tests ensure we're running in the expected environment and will fail if we don't, which helps understand the actual issue. + +Read more [Ability to run spec files in a specific order](https://github.com/cypress-io/cypress/issues/390) + +Work around to order your tests: [Cypress - How can I run test files in order](https://stackoverflow.com/questions/58936891/cypress-how-can-i-run-test-files-in-order/59690611#59690611) diff --git a/cypress/integration-storybook/app/stories/dataDisplay/btn.ts b/cypress/integration-storybook/app/stories/dataDisplay/btn.ts new file mode 100644 index 000000000..0aca78e43 --- /dev/null +++ b/cypress/integration-storybook/app/stories/dataDisplay/btn.ts @@ -0,0 +1,30 @@ +describe('Btn story', () => { + /** + * Visits the story page before any test. + */ + before(() => { + cy.visit('/?path=/story/next-right-now-welcome-to-nrn--page'); + }); + + /** + * XXX The role of this test is to make sure to detect regressions affecting the whole Storybook site. + * We don't intend to test all components here, we only want to be warned if we ever break Storybook. + */ + it('should have a "Data display" menu on the left navigation panel', () => { + cy.get('#next-right-now-data-display').should('have.length', 1).should('have.text', 'Data display').click(); + }); + + it('should have a "Data display" > "Btn" story', () => { + cy.get('#next-right-now-data-display-btn').should('have.length', 1).should('have.text', 'Btn').click(); + }); + + it('should have a writable "#text" control property', () => { + cy.get('#text').should('have.length', 1).should('have.text', 'Hello').type(' Cypress!'); + }); + + it('should have changed the Btn text to "Hello Cypress!', () => { + cy.findIframe('iframe#storybook-preview-iframe').find('#root button').should('have.text', 'Hello Cypress!') + }); +}); + +export {}; diff --git a/cypress/support/commands.d.ts b/cypress/support/commands.d.ts index 7dbe007be..66c0bfe79 100644 --- a/cypress/support/commands.d.ts +++ b/cypress/support/commands.d.ts @@ -1,5 +1,6 @@ declare namespace Cypress { interface cy extends Chainable { prepareDOMAliases: () => Chainable; + findIframe: (iframeSelector: string) => Chainable; } } diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 6d46eb582..08637a81a 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -31,3 +31,25 @@ Cypress.Commands.add('prepareDOMAliases', () => { }); }); }); + +/** + * Finds an iframe and returns it "body" HTML element. + * + * XXX Alternatively, look for the cypress-iframe NPM plugin if you need more iframe-related features! See https://www.npmjs.com/package/cypress-iframe + * + * @example cy.findIframe(); + * @see https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/ + */ +Cypress.Commands.add('findIframe', (iframeSelector) => { + // get the iframe > document > body + // and retry until the body element is not empty + cy.log('findIframe') + + return cy + .get(iframeSelector, { log: false }) + .its('0.contentDocument.body', { log: false }).should('not.be.empty') + // wraps "body" DOM element to allow + // chaining more Cypress commands, like ".find(...)" + // https://on.cypress.io/wrap + .then((body) => cy.wrap(body, { log: false })) +}) diff --git a/package.json b/package.json index 7feebbf50..cd33c949d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "deploy:all:production:simple": "yarn deploy:customer1:production:simple && yarn deploy:customer2:production:simple", "deploy:all:all": "yarn deploy:all && yarn deploy:all:production", "deploy:ci:gha": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.${CUSTOMER_REF}.staging.json", + "deploy:sb:gha": "yarn vercel:cleanup && yarn storybook:export && vercel storybook-static --local-config=vercel.storybook.json --confirm --debug", + "deploy:sb:gha:fast": "yarn vercel:cleanup && vercel storybook-static --local-config=vercel.storybook.json --confirm --debug", "deploy:customer:production:simple": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.${CUSTOMER_REF}.production.json --prod", "deploy:customer1:all": "yarn deploy:customer1 && yarn deploy:customer1:production", "deploy:customer1": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer1.staging.json", @@ -45,6 +47,9 @@ "e2e:run": "CYPRESS_STAGE=${CYPRESS_STAGE:-development}; cypress run --config-file cypress/config-$CYPRESS_STAGE.json", "e2e:run:spec:common": "CYPRESS_STAGE=${CYPRESS_STAGE:-development}; cypress run --config-file cypress/config-$CYPRESS_STAGE.json --spec 'cypress/integration/app/common/*.js'", "e2e:ci": "yarn e2e:install && cypress run --record", + "e2e:sb": "", + "e2e:sb:run": "cypress run --config-file cypress/config-storybook.json", + "e2e:sb:open": "cypress open --config-file cypress/config-storybook.json", "doc:start": "cd docs/ && bundle exec jekyll serve --config _config-development.yml", "doc:start:fast": "cd docs/ && bundle exec jekyll serve --config _config-development.yml --incremental", "doc:gem:install": "cd docs/ && bundle install", @@ -68,6 +73,10 @@ "test:coverage": "NODE_ENV=test jest --coverage", "test:coverage:group:no-integration": "NODE_ENV=test jest --group=-integration --coverage", "test:config": "NODE_ENV=test jest --showConfig", + "sb": "yarn storybook", + "storybook": "STORYBOOK=true start-storybook -h localhost -p 6006 --no-manager-cache", + "storybook:export": "STORYBOOK=true build-storybook", + "sb:upgrade": "sb upgrade", "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/", @@ -167,8 +176,19 @@ }, "devDependencies": { "@cypress/react": "4.16.3", + "@emotion/babel-preset-css-prop": "11.0.0", "@emotion/eslint-plugin": "11.0.0", "@next/bundle-analyzer": "10.0.5", + "@storybook/addon-a11y": "6.1.14", + "@storybook/addon-actions": "6.1.14", + "@storybook/addon-console": "1.2.2", + "@storybook/addon-essentials": "6.1.14", + "@storybook/addon-google-analytics": "6.1.14", + "@storybook/addon-links": "6.1.14", + "@storybook/addon-storysource": "6.1.14", + "@storybook/addons": "6.1.14", + "@storybook/react": "6.1.14", + "@storybook/theming": "6.1.14", "@svgr/cli": "5.5.0", "@types/amplitude-js": "7.0.0", "@types/cookies": "0.7.6", @@ -195,6 +215,7 @@ "@types/popper.js": "1.11.0", "@types/react": "17.0.0", "@types/react-test-renderer": "17.0.0", + "@types/reactstrap": "8.7.2", "@types/uuid": "8.3.0", "@types/webfontloader": "1.6.32", "@types/webpack-env": "1.16.0", @@ -221,9 +242,15 @@ "jest-to-match-shape-of": "1.3.1", "next-unused": "0.0.3", "ngrok": "3.4.0", + "node-fetch": "2.6.1", "node-mocks-http": "1.9.0", "open-cli": "6.0.1", "react-test-renderer": "17.0.1", + "storybook-addon-designs": "5.4.2", + "storybook-addon-next-router": "2.0.3", + "storybook-addon-performance": "0.13.0", + "storybook-css-modules-preset": "1.0.5", + "storybook-mobile": "0.1.29", "ts-jest": "26.4.4", "typescript": "4.1.3", "vercel": "21.0.1" diff --git a/src/components/animations/Loader.tsx b/src/components/animations/Loader.tsx index d92d7ff74..b83356855 100644 --- a/src/components/animations/Loader.tsx +++ b/src/components/animations/Loader.tsx @@ -1,9 +1,8 @@ import { css } from '@emotion/react'; import React from 'react'; - import AnimatedLoader from '../svg/AnimatedLoader'; -type Props = {} +export type Props = {} const Loader: React.FunctionComponent = (props): JSX.Element => { return ( diff --git a/src/components/appBootstrap/BrowserPageBootstrap.tsx b/src/components/appBootstrap/BrowserPageBootstrap.tsx index b9679e9c2..a5246467e 100644 --- a/src/components/appBootstrap/BrowserPageBootstrap.tsx +++ b/src/components/appBootstrap/BrowserPageBootstrap.tsx @@ -2,10 +2,10 @@ import { Amplitude, AmplitudeProvider, } from '@amplitude/react-amplitude'; +import { useTheme } from '@emotion/react'; import * as Sentry from '@sentry/node'; import { createLogger } from '@unly/utils-simple-logger'; import { AmplitudeClient } from 'amplitude-js'; -import { useTheme } from '@emotion/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; import useCustomer from '../../hooks/useCustomer'; @@ -15,7 +15,6 @@ import { cypressContext } from '../../stores/cypressContext'; import userConsentContext from '../../stores/userConsentContext'; import { userSessionContext } from '../../stores/userSessionContext'; import { Customer } from '../../types/data/Customer'; -import { CustomerTheme } from '../../types/data/CustomerTheme'; import { MultiversalAppBootstrapPageProps } from '../../types/nextjs/MultiversalAppBootstrapPageProps'; import { MultiversalAppBootstrapProps } from '../../types/nextjs/MultiversalAppBootstrapProps'; import { MultiversalPageProps } from '../../types/pageProps/MultiversalPageProps'; @@ -61,7 +60,10 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => lang, locale, } = pageProps; - const { t, i18n } = useTranslation(); + const { + t, + i18n, + } = useTranslation(); const dataset = useDataset(); const customer: Customer = useCustomer(); const isInIframe: boolean = isRunningInIframe(); @@ -89,7 +91,10 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => }); const userConsent: UserConsent = getUserConsent(); - const { isUserOptedOutOfAnalytics, hasUserGivenAnyCookieConsent } = userConsent; + const { + isUserOptedOutOfAnalytics, + hasUserGivenAnyCookieConsent, + } = userConsent; const amplitudeInstance: AmplitudeClient = getAmplitudeInstance({ customerRef, iframeReferrer, diff --git a/src/components/appBootstrap/MultiversalAppBootstrap.tsx b/src/components/appBootstrap/MultiversalAppBootstrap.tsx index 1dc493c52..cbc914a6a 100644 --- a/src/components/appBootstrap/MultiversalAppBootstrap.tsx +++ b/src/components/appBootstrap/MultiversalAppBootstrap.tsx @@ -367,8 +367,18 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele return ( - - + + {/* XXX Global styles that applies to all pages go there */} diff --git a/src/components/appBootstrap/MultiversalGlobalExternalStyles.tsx b/src/components/appBootstrap/MultiversalGlobalExternalStyles.tsx new file mode 100644 index 000000000..5cb506ba1 --- /dev/null +++ b/src/components/appBootstrap/MultiversalGlobalExternalStyles.tsx @@ -0,0 +1,12 @@ +/** + * Contains all imports of external CSS libs (.css files) that must be injected in all Next.js pages. + * + * This approach is preferred over importing them all one by one within the _app.tsx file, because it's easier to maintain. + * + * Also, this file is being imported by both "src/pages/_app.tsx" and ".storybook/preview.js", + * so that global 3rd party CSS are included when previewing components, too. + */ +import 'animate.css/animate.min.css'; // Loads animate.css CSS file. See https://github.com/daneden/animate.css +import 'bootstrap/dist/css/bootstrap.min.css'; // Loads bootstrap CSS file. See https://stackoverflow.com/a/50002905/2391795 +import 'cookieconsent/build/cookieconsent.min.css'; // Loads CookieConsent CSS file. See https://github.com/osano/cookieconsent +import 'rc-tooltip/assets/bootstrap.css'; diff --git a/src/components/appBootstrap/MultiversalGlobalStyles.tsx b/src/components/appBootstrap/MultiversalGlobalStyles.tsx index e6e005431..929dc9842 100644 --- a/src/components/appBootstrap/MultiversalGlobalStyles.tsx +++ b/src/components/appBootstrap/MultiversalGlobalStyles.tsx @@ -60,7 +60,9 @@ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Ele } // Only applied to the main application - body.nrn { + body.nrn, + // And when included within Storybook + body.sb-show-main { background-color: ${backgroundColor}; .page-container { @@ -98,18 +100,19 @@ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Ele // Applied to all elements marked with ".nrn" // Those will be applied even into iframes. // If there are iframes being displayed, they'll inherit the below behaviors. - .nrn { + .nrn, + .sb-show-main { // ----------- Application-wide custom elements ----------- // ----------- Color system utilities ----------- a { - color: ${primaryColor}; + color: ${primaryColor}; - &:hover { - color: ${primaryColorVariant1}; - } + &:hover { + color: ${primaryColorVariant1}; + } } .pcolor, @@ -172,7 +175,7 @@ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Ele cursor: pointer; &:focus { - outline: 0; // Overrides Bootstrap default behavior + outline: 0; // Overrides Bootstrap default behavior } &.disabled { @@ -247,7 +250,7 @@ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Ele // ----------- React-Select override ----------- .select { - * { + * { color: ${primaryColor} !important; } } diff --git a/src/components/errors/DefaultErrorLayout.tsx b/src/components/errors/DefaultErrorLayout.tsx index c1eb88483..886323d02 100644 --- a/src/components/errors/DefaultErrorLayout.tsx +++ b/src/components/errors/DefaultErrorLayout.tsx @@ -6,7 +6,7 @@ import { GenericObject } from '../../types/GenericObject'; import ErrorDebug from './ErrorDebug'; -type Props = { +export type Props = { error: Error; context?: GenericObject; } @@ -20,7 +20,10 @@ type Props = { * @param props */ const DefaultErrorLayout = (props: Props): JSX.Element => { - const { error, context } = props; + const { + error, + context, + } = props; const errorEventId = Sentry.captureException(error); return ( diff --git a/src/components/errors/ErrorDebug.tsx b/src/components/errors/ErrorDebug.tsx index 66fa5d9bc..fa6af383e 100644 --- a/src/components/errors/ErrorDebug.tsx +++ b/src/components/errors/ErrorDebug.tsx @@ -39,6 +39,8 @@ const ErrorDebug = (props: Props): JSX.Element => { Note that debug information about the error are also available on the server/browser console. +
+

Debug information:

 void;
 }
 
diff --git a/src/components/i18n/I18nLink.tsx b/src/components/i18n/I18nLink.tsx
index 539267242..d6ebdb2a7 100644
--- a/src/components/i18n/I18nLink.tsx
+++ b/src/components/i18n/I18nLink.tsx
@@ -12,26 +12,102 @@ import {
 
 type ParamValueToForward = string | number | Array;
 
-type Props = {
+export type Props = {
+  /**
+   * Optional decorator for the path that will be shown in the browser URL bar.
+   */
   as?: string;
+
+  /**
+   * React node as children.
+   */
   children: React.ReactNode;
+
+  /**
+   * Additional CSS classes.
+   */
   className?: string;
+
+  /**
+   * The path or URL to navigate to.
+   */
   href: string;
+
+  /**
+   * The active locale is automatically prepended. locale allows for providing a different locale.
+   *
+   * @default current locale
+   */
   locale?: string; // The locale can be specified, but it'll fallback to the current locale if unspecified
+
+  /**
+   * Parameters to inject into the url, necessary when using route params (other than `locale`).
+   *
+   * Example:
+   *
+   * `/products/[id]` with `params={{ id: 5 }}` becomes `/products/5`
+   */
   params?: { [key: string]: ParamValueToForward };
+
+  /**
+   * Forces Link to send the href property to its child.
+   *
+   * @default false
+   */
   passHref?: boolean;
+
+  /**
+   * Prefetch the page in the background.
+   * Any  that is in the viewport (initially or through scroll) will be preloaded.
+   * Prefetch can be disabled by passing prefetch={false}.
+   * Pages using Static Generation will preload JSON files with the data for faster page transitions.
+   *
+   * @default true
+   */
   prefetch?: boolean;
+
+  /**
+   * Query to inject to the url, necessary when using route query param.
+   *
+   * Example:
+   *
+   * `/products` with `query={{ userId: 1 }}` becomes `/products?userId=1`
+   */
   query?: { [key: string]: ParamValueToForward };
+
+  /**
+   * Replace the current history state instead of adding a new url into the stack.
+   *
+   * @default false
+   */
   replace?: boolean;
+
+  /**
+   * Scroll to the top of the page after a navigation.
+   *
+   * @default true
+   */
   scroll?: boolean;
+
+  /**
+   * Update the path of the current page without rerunning `getStaticProps`, `getServerSideProps` or `getInitialProps`.
+   *
+   * @default false
+   */
   shallow?: boolean;
+
+  /**
+   * Disabled on Storybook, as it crashes the UI.
+   */
   wrapChildrenAsLink?: boolean; // Helper to avoid writing redundant code
 };
 
 /**
  * Wrapper around the native Next.js  component. Handles localised links.
  *
- * Use the current active locale by default
+ * Uses the current active locale by default.
+ *
+ * [Read why we don't use the official Next.js `Link` component](https://unlyed.github.io/next-right-now/guides/i18n/#official-i18n-routing-implementation) Tip
  *
  * @example Recommended usage
  *  Homepage
diff --git a/src/components/pageLayouts/Footer.tsx b/src/components/pageLayouts/Footer.tsx
index e11dba8d1..8f2f7b14b 100644
--- a/src/components/pageLayouts/Footer.tsx
+++ b/src/components/pageLayouts/Footer.tsx
@@ -1,21 +1,21 @@
-import { css } from '@emotion/react';
-import { useTheme } from '@emotion/react';
+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 { CustomerTheme } from '../../types/data/CustomerTheme';
 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';
 
-type Props = {
+export type Props = {
   style?: CSSStyles;
 };
 
diff --git a/src/components/pageLayouts/Nav.tsx b/src/components/pageLayouts/Nav.tsx
index 999bea851..cbdeb217b 100644
--- a/src/components/pageLayouts/Nav.tsx
+++ b/src/components/pageLayouts/Nav.tsx
@@ -1,8 +1,10 @@
 import { Amplitude } from '@amplitude/react-amplitude';
-import { css } from '@emotion/react';
+import {
+  css,
+  useTheme,
+} from '@emotion/react';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import classnames from 'classnames';
-import { useTheme } from '@emotion/react';
 import kebabCase from 'lodash.kebabcase';
 import map from 'lodash.map';
 import {
@@ -26,8 +28,8 @@ import {
 import useI18n, { I18n } from '../../hooks/useI18n';
 import customerContext, { CustomerContext } from '../../stores/customerContext';
 import { LogEvent } from '../../types/Amplitude';
+import { AirtableAttachment } from '../../types/data/AirtableAttachment';
 import { Asset } from '../../types/data/Asset';
-import { CustomerTheme } from '../../types/data/CustomerTheme';
 import { SidebarLink } from '../../types/SidebarLink';
 import {
   isActive,
@@ -40,12 +42,17 @@ import { NATIVE_FEATURES_SIDEBAR_LINKS } from '../doc/NativeFeaturesSidebar';
 import I18nLink from '../i18n/I18nLink';
 import Tooltip from '../utils/Tooltip';
 
-type Props = {};
+export type Props = {};
 
 const Nav: React.FunctionComponent = () => {
   const { t } = useTranslation();
   const router: NextRouter = useRouter();
   const theme = useTheme();
+  const {
+    primaryColor,
+    logo: logoAirtable,
+  } = theme;
+  const logo: AirtableAttachment = logoAirtable;
   const { locale }: I18n = useI18n();
   const { primaryColor, logo } = theme;
 
@@ -83,12 +90,12 @@ const Nav: React.FunctionComponent = () => {
             }
 
             .navItemsMenu {
-              padding:0 10px;
+              padding: 0 10px;
 
               @media (max-width: 991.98px) {
                 a {
                   font-size: 12px;
-                  color: rgba(0,0,0,0.30) !important;
+                  color: rgba(0, 0, 0, 0.30) !important;
                 }
               }
             }
@@ -150,7 +157,11 @@ const Nav: React.FunctionComponent = () => {
              = () => {
                     Native features
                     {
                       map(NATIVE_FEATURES_SIDEBAR_LINKS, (link: SidebarLink) => {
-                        const { label, href, params = null } = link;
+                        const {
+                          label,
+                          href,
+                          params = null,
+                        } = link;
                         return (
                           
                             
@@ -206,7 +221,11 @@ const Nav: React.FunctionComponent = () => {
                     Built-in features
                     {
                       map(BUILT_IN_FEATURES_SIDEBAR_LINKS, (link: SidebarLink) => {
-                        const { label, href, params = null } = link;
+                        const {
+                          label,
+                          href,
+                          params = null,
+                        } = link;
                         return (
                           
                             
@@ -223,7 +242,11 @@ const Nav: React.FunctionComponent = () => {
                     Built-in utilities
                     {
                       map(BUILT_IN_UTILITIES_SIDEBAR_LINKS, (link: SidebarLink) => {
-                        const { label, href, params = null } = link;
+                        const {
+                          label,
+                          href,
+                          params = null,
+                        } = link;
                         return (
                           
                             
diff --git a/src/components/pageLayouts/PreviewModeBanner.tsx b/src/components/pageLayouts/PreviewModeBanner.tsx
index 6706e8989..f20afdc2f 100644
--- a/src/components/pageLayouts/PreviewModeBanner.tsx
+++ b/src/components/pageLayouts/PreviewModeBanner.tsx
@@ -1,6 +1,8 @@
-import { css } from '@emotion/react';
+import {
+  css,
+  useTheme,
+} from '@emotion/react';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { useTheme } from '@emotion/react';
 import {
   NextRouter,
   useRouter,
@@ -10,21 +12,19 @@ import {
   Trans,
   useTranslation,
 } from 'react-i18next';
-
 import usePreviewMode, { PreviewMode } from '../../hooks/usePreviewMode';
-import { CustomerTheme } from '../../types/data/CustomerTheme';
 import { stringifyQueryParameters } from '../../utils/app/router';
 import {
   startPreviewMode,
   stopPreviewMode,
 } from '../../utils/nextjs/previewMode';
-import ExternalLink from '../utils/ExternalLink';
 import Btn from '../utils/Btn';
+import ExternalLink from '../utils/ExternalLink';
 import Tooltip from '../utils/Tooltip';
 
-type Props = {}
+export type Props = {}
 
-const ExplanationTooltipOverlay: React.FunctionComponent = (): JSX.Element => {
+export const ExplanationTooltipOverlay: React.FunctionComponent = (): JSX.Element => {
   return (
      = (props): JSX.Element =
   const queryParameters: string = stringifyQueryParameters(router);
   const { t } = useTranslation();
   const {
-    secondaryColor, secondaryColorVariant1, onSecondaryColor,
+    secondaryColor,
+    secondaryColorVariant1,
+    onSecondaryColor,
   } = useTheme();
 
   if (process.env.NEXT_PUBLIC_APP_STAGE === 'production') {
diff --git a/src/components/pageLayouts/QuickPreviewBanner.tsx b/src/components/pageLayouts/QuickPreviewBanner.tsx
index b1be9944b..979bee6a9 100644
--- a/src/components/pageLayouts/QuickPreviewBanner.tsx
+++ b/src/components/pageLayouts/QuickPreviewBanner.tsx
@@ -1,7 +1,9 @@
-import { css } from '@emotion/react';
+import {
+  css,
+  useTheme,
+} from '@emotion/react';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { createLogger } from '@unly/utils-simple-logger';
-import { useTheme } from '@emotion/react';
 import {
   NextRouter,
   useRouter,
@@ -10,7 +12,6 @@ import React, { Fragment } from 'react';
 import { useTranslation } from 'react-i18next';
 import useCustomer from '../../hooks/useCustomer';
 import { Customer } from '../../types/data/Customer';
-import { CustomerTheme } from '../../types/data/CustomerTheme';
 import I18nBtnChangeLocale from '../i18n/I18nBtnChangeLocale';
 import Btn from '../utils/Btn';
 import Tooltip from '../utils/Tooltip';
@@ -20,7 +21,7 @@ const logger = createLogger({
   label: fileLabel,
 });
 
-type Props = {
+export type Props = {
   ExplanationTooltipOverlay?: React.FunctionComponent;
   LeftActions?: React.FunctionComponent;
   quickPreviewTitle?: string;
@@ -44,7 +45,9 @@ const QuickPreviewBanner: React.FunctionComponent = (props): JSX.Element
     quickPreviewTitle,
   } = props;
   const {
-    secondaryColor, secondaryColorVariant1, onSecondaryColor,
+    secondaryColor,
+    secondaryColorVariant1,
+    onSecondaryColor,
   } = useTheme();
   const { t } = useTranslation();
   const router: NextRouter = useRouter();
@@ -87,7 +90,7 @@ const QuickPreviewBanner: React.FunctionComponent = (props): JSX.Element
               align-items: center;
             }
           }
-      `}
+        `}
       >
         
{ diff --git a/src/components/rehydration/DisplayOnBrowserMount.tsx b/src/components/rehydration/DisplayOnBrowserMount.tsx index 7d444e7e7..100f38665 100644 --- a/src/components/rehydration/DisplayOnBrowserMount.tsx +++ b/src/components/rehydration/DisplayOnBrowserMount.tsx @@ -6,7 +6,7 @@ import React, { useState, } from 'react'; -type Props = { +export type Props = { children: React.ReactNode; deps?: DependencyList; }; diff --git a/src/components/svg/Animated3Dots.tsx b/src/components/svg/Animated3Dots.tsx index 41c4e04a4..09050a368 100644 --- a/src/components/svg/Animated3Dots.tsx +++ b/src/components/svg/Animated3Dots.tsx @@ -1,5 +1,9 @@ import React from 'react'; +export type Props = { + fill: string; +}; + /** * An animated composant featuring 3 animated dots "...". * @@ -8,7 +12,7 @@ import React from 'react'; * * @see https://animate.style */ -const Animated3Dots = (props): JSX.Element => { +const Animated3Dots = (props: Props): JSX.Element => { return ( { +export type Props = { + fill?: string; +}; + +const AnimatedTextBubble = (props: Props) => { const theme = useTheme(); const { surfaceColor } = theme; + return (
; const SvgEnglishHybridFlag = (props: Props): JSX.Element => { return ( + // @ts-ignore { diff --git a/src/components/svg/EnglishUkFlag.tsx b/src/components/svg/EnglishUkFlag.tsx index bbbf89de4..8193f4307 100644 --- a/src/components/svg/EnglishUkFlag.tsx +++ b/src/components/svg/EnglishUkFlag.tsx @@ -1,8 +1,10 @@ -import React from "react"; +import React from 'react'; + type Props = {} & React.SVGProps; const SvgEnglishUkFlag = (props: Props): JSX.Element => { return ( + // @ts-ignore { diff --git a/src/components/utils/Btn.tsx b/src/components/utils/Btn.tsx index f39435336..b1439b6cc 100644 --- a/src/components/utils/Btn.tsx +++ b/src/components/utils/Btn.tsx @@ -1,8 +1,9 @@ -import { css } from '@emotion/react'; +import { + css, + useTheme, +} from '@emotion/react'; import classnames from 'classnames'; -import { useTheme } from '@emotion/react'; import React, { ReactNode } from 'react'; -import { CustomerTheme } from '../../types/data/CustomerTheme'; import { ReactButtonProps } from '../../types/react/ReactButtonProps'; import { ComponentThemeMode, @@ -10,29 +11,29 @@ import { ThemedComponentProps, } from '../../utils/theming/themedComponentColors'; -type Props = { +export type Props = { + /** + * What's being displayed within the button. + */ children: ReactNode; + + /** + * Always adds the "btn" class, more CSS classes can be added. + */ className?: string; } & ReactButtonProps & ThemedComponentProps; /** - * "Call to Action" button. - * - * Button meant to highlight a potential user interaction. - * Themed component. - * - * Used for: - * - Navigate to another page - * - Validate an action + * Flexible HTML `button` component that can take many shapes and be used with various colors and background colors. * - * @param props + * Implements `ThemedComponentColors`. */ const Btn: React.FunctionComponent = (props): JSX.Element => { const { children, className, mode = 'primary' as ComponentThemeMode, - transparent, + isTransparent, ...rest } = props; const customerTheme = useTheme(); @@ -44,7 +45,7 @@ const Btn: React.FunctionComponent = (props): JSX.Element => { hoverBackgroundColor, hoverBorderColor, hoverBoxShadowColor, - } = resolveThemedComponentColors(customerTheme, mode, transparent); + } = resolveThemedComponentColors(customerTheme, mode, isTransparent); return (
-
-
- -     - - Default - -     - - Default transparent - -     - - Reverse - -     - - Reverse transparent - -     - - Outline - -     - - Outline transparent - -     - - Variant - - - -     - - Secondary - -     - - Secondary transparent - -     - - Secondary Reverse - -     - - Secondary Reverse transparent - -     - - Secondary Outline - -     - - Secondary Outline transparent - -     - - Secondary Variant - - -
-
-
- -     - - Default - -     - - Default transparent - -     - - Reverse - -     - - Reverse transparent - -     - - Outline - -     - - Outline transparent - -     - - Variant - - - -     - - Secondary - -     - - Secondary transparent - -     - - Secondary Reverse - -     - - Secondary Reverse transparent - -     - - Secondary Outline - -     - - Secondary Outline transparent - -     - - Secondary Variant - - -
-
-
- - ); -}; - -export default Buttons; diff --git a/src/components/utils/Cards.tsx b/src/components/utils/Cards.tsx index 38ed70328..050975052 100644 --- a/src/components/utils/Cards.tsx +++ b/src/components/utils/Cards.tsx @@ -3,18 +3,31 @@ import classnames from 'classnames'; import React, { ReactNode } from 'react'; import { CardDeck } from 'reactstrap'; -type Props = { +export type Props = { + /** + * Must be an array of `Card`. + */ children: ReactNode; - maxCards?: number; // Max cards per row + + /** + * Max cards per row. + * Current implementation only support `2 | 3`. + * + * @default 3 + */ + maxCards?: number; } /** - * Wrapper for Reactstrap component, to display cards as a Deck and apply common styling on all cards + * Wrapper for Reactstrap `Card` component, to display cards as a Deck and apply common styling on all cards. * * @param props */ const Cards: React.FunctionComponent = (props): JSX.Element => { - const { children, maxCards = 3 } = props; + const { + children, + maxCards = 3, + } = props; return ( = (props): JSX.Element => { const { children, margin = '0', mode = 'primary-reverse' as ComponentThemeMode, - transparent, + isTransparent, ...rest } = props; const customerTheme = useTheme(); @@ -37,10 +44,11 @@ const CircleBtn: React.FunctionComponent = (props): JSX.Element => { color, backgroundColor, borderColor, - } = resolveThemedComponentColors(customerTheme, mode, transparent); + } = resolveThemedComponentColors(customerTheme, mode, isTransparent); return (
= (props): JSX.Element => { position: relative; width: 30px; // XXX Related to the width of the root component, must be higher than "max-width - min-width" } - `} + `} {...rest} >
diff --git a/src/components/utils/Code.tsx b/src/components/utils/Code.tsx index 0ebc1e5f9..0ab2ee2de 100644 --- a/src/components/utils/Code.tsx +++ b/src/components/utils/Code.tsx @@ -3,13 +3,25 @@ import { CodeBlock, dracula, } from 'react-code-blocks'; +import { CodeBlockProps } from 'react-code-blocks/dist/components/CodeBlock'; import { CSSStyles } from '../../types/CSSStyles'; -type Props = { +export type Props = { + /** + * The style object to apply to the `CodeBlock` text directly i.e `fontSize` and such. [See `codeBlockStyle` type](https://github.com/rajinwonderland/react-code-blocks/blob/31e391b30a1f2835aaad4275f542329239761182/packages/react-code-blocks/src/components/CodeBlock.tsx#L19) + */ codeBlockStyle?: CSSStyles; + + /** + * The style object that accesses the style parameter on the `codeTagProps` property on the `Code` component. [See `codeContainerStyle` type](https://github.com/rajinwonderland/react-code-blocks/blob/31e391b30a1f2835aaad4275f542329239761182/packages/react-code-blocks/src/components/CodeBlock.tsx#L19) + */ codeContainerStyle?: CSSStyles; + + /** + * The code to be formatted. + */ text: string; -} +} & Partial; const defaultCodeBlockStyle = { textAlign: 'left', @@ -20,14 +32,20 @@ const defaultCodeContainerStyle = { }; /** - * Display "text" property as source code, using the "react-code-blocks" library + * Displays "text" property as source code, using [`react-code-blocks` library](https://github.com/rajinwonderland/react-code-blocks). + * + * Pre-configured with theme color, and sane default options for ease of use. * - * Pre-configured with theme color, and default sane options for ease of use + * All props aren't specified in their documentation. [See `CodeBlockProps` type](https://github.com/rajinwonderland/react-code-blocks/blob/31e391b30a1f2835aaad4275f542329239761182/packages/react-code-blocks/src/components/CodeBlock.tsx#L6-L35) * * @param props */ const Code: React.FunctionComponent = (props): JSX.Element => { - const { codeBlockStyle, codeContainerStyle, text } = props; + const { + codeBlockStyle, + codeContainerStyle, + text, + } = props; return ( = (props): JSX.Element => { showLineNumbers={true} theme={dracula} wrapLines + {...props} /> ); }; diff --git a/src/components/utils/DocumentButton.tsx b/src/components/utils/DocumentButton.tsx index 2576f151a..f4871628b 100644 --- a/src/components/utils/DocumentButton.tsx +++ b/src/components/utils/DocumentButton.tsx @@ -1,10 +1,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { ReactNode } from 'react'; import Btn from './Btn'; -import Tooltip from './Tooltip'; import EllipsisText from './EllipsisText'; +import Tooltip from './Tooltip'; -type Props = { +export type Props = { + /** + * React children, usually text. + */ children: ReactNode; } diff --git a/src/components/utils/EllipsisText.tsx b/src/components/utils/EllipsisText.tsx index ab1ec0f94..510b2a42a 100644 --- a/src/components/utils/EllipsisText.tsx +++ b/src/components/utils/EllipsisText.tsx @@ -1,12 +1,49 @@ import { css } from '@emotion/react'; import React, { ReactNode } from 'react'; -type Props = { +export type Props = { + /** + * React children, usually text. + */ children: ReactNode; + + /** + * Text to display as HTML title attribute. + */ + title?: string; + + // XXX Implementation idea: Allow to configure the title to display as tooltip instead of HTML title (prettier) + // titleAsTooltip?: boolean; + + /** + * Width on large devices. + * + * @default 140px + */ widthLarge?: string; + + /** + * Width on medium devices. + * + * @default 140px + */ widthMedium?: string; + + /** + * Width on small devices. + * + * @default 140px + */ widthSmall?: string; - forceSingleLine?: boolean; // XXX Not handled yet, see below TODO + + /** + * Option to force children (expect text) in a single line. + * + * Deprecated Not properly implemented. Unstable. + * + * @deprecated + */ + forceSingleLine?: boolean; } /** @@ -15,8 +52,6 @@ type Props = { * Helps avoiding long text taking too much space and basically crop it instead. * Forces text to display in a single line by default. * - * TODO Case when forceSingleLine: false isn't handled yet (Léo) - * * @param props */ const EllipsisText: React.FunctionComponent = (props): JSX.Element => { @@ -25,11 +60,15 @@ const EllipsisText: React.FunctionComponent = (props): JSX.Element => { widthMedium = '140px', widthSmall = '140px', forceSingleLine = true, - children + title, + children, } = props; + const dynamicProps = {}; + return (
void; + + /** + * Helper to avoid link to "stick" with text. + * + * @default " " (whitespace) + */ prefix?: string; + + /** + * Helper to avoid link to "stick" with text. + * + * @default " " (whitespace) + */ suffix?: string; -} +} & Partial; /** - * Link that point to an external website - * - * Use sane default for proper SEO (noreferrer disabled by default), security (nofollow enabled by default) and display (prefix/suffix) + * Link that point to an external website. * - * @param props + * Use sane default for proper SEO (noreferrer disabled by default), security (nofollow enabled by default) and display (prefix/suffix). */ const ExternalLink: React.FunctionComponent = (props): JSX.Element => { const { children, href, - nofollow = true, // Tell bots not to follow the link when crawling (you may want to disable this depending on your use case) - noopener = true, // Security, avoids external site opened through your site to have control over your site (always apply "noopener" unless you know what you're doing) - noreferrer = false, // SEO, avoids external site opened through your site to know they have been opened from your site (don't apply "noreferrer" unless you know what you're doing) - prefix = ' ', // Helper to avoid link to "stick" with text - suffix = ' ', // Helper to avoid link to "stick" with text + nofollow = true, + noopener = true, + noreferrer = false, + prefix = ' ', + suffix = ' ', ...rest } = props; return ( {prefix} + {/*// @ts-ignore*/} = (props): JSX.Element => { - return ( -
- {` - This component is a template meant to be duplicated to quickly get started with new React components. - - Feel free to adapt it at your convenience - `} -
- ); -}; - -export default ComponentTemplate; diff --git a/src/components/utils/LegalContent.tsx b/src/components/utils/LegalContent.tsx index 988c74363..25c6d3d05 100644 --- a/src/components/utils/LegalContent.tsx +++ b/src/components/utils/LegalContent.tsx @@ -1,12 +1,13 @@ -import { css } from '@emotion/react'; -import { useTheme } from '@emotion/react'; +import { + css, + useTheme, +} from '@emotion/react'; import React from 'react'; import { Container } from 'reactstrap'; -import { CustomerTheme } from '../../types/data/CustomerTheme'; import { Markdown as TextAsMarkdown } from '../../types/Markdown'; import Markdown from './Markdown'; -type Props = { +export type Props = { content: TextAsMarkdown; } @@ -18,7 +19,7 @@ type Props = { const LegalContent: React.FunctionComponent = (props): JSX.Element => { const theme = useTheme(); const { primaryColor } = theme; - const { content} = props; + const { content } = props; return ( @@ -28,31 +29,31 @@ const LegalContent: React.FunctionComponent = (props): JSX.Element => { margin: 50px 150px 150px; h1 { - color: ${primaryColor}; - font-size: 35px; + color: ${primaryColor}; + font-size: 35px; } h2 { - font-size: 20px; - margin-top: 35px; + font-size: 20px; + margin-top: 35px; } h3 { - font-size: 17px; + font-size: 17px; } h4 { - font-size: 13px; - font-weight: 300; + font-size: 13px; + font-weight: 300; } h5 { - font-size: 13px; - font-weight: 100; + font-size: 13px; + font-weight: 100; } h6 { - font-size: 10px; + font-size: 10px; } table { diff --git a/src/components/utils/LinkButton.tsx b/src/components/utils/LinkButton.tsx index 2a92e4caf..61aaa35c5 100644 --- a/src/components/utils/LinkButton.tsx +++ b/src/components/utils/LinkButton.tsx @@ -2,10 +2,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { ReactNode } from 'react'; import Btn from './Btn'; -type Props = { +export type Props = { + /** + * React children, usually text. + */ children: ReactNode; } +/** + * A button with a Font-Awesome `link` icon as prefix. + */ const LinkButton: React.FunctionComponent = (props): JSX.Element => { const { children } = props; return ( diff --git a/src/components/utils/Markdown.tsx b/src/components/utils/Markdown.tsx index a5b23769c..005911f15 100644 --- a/src/components/utils/Markdown.tsx +++ b/src/components/utils/Markdown.tsx @@ -21,6 +21,7 @@ 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'; @@ -28,10 +29,29 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- label: fileLabel, }); -type Props = { +export type Props = { + /** + * Markdown as text. + * + * Usually comes from a content source, such as a CMS, file, etc. + */ text: MarkdownType; + + /** + * Parsing options of the [`markdown-to-jsx` library](https://github.com/probablyup/markdown-to-jsx/blob/master/README.md#parsing-options). + * + * @see https://github.com/probablyup/markdown-to-jsx/blob/master/README.md#parsing-options + */ markdownOptions?: MarkdownToJSX.Options; + + /** + * CSS styles. + */ style?: CSSStyles; + + /** + * CSS classes. + */ className?: string; } @@ -61,6 +81,7 @@ const defaultMarkdownOptions: MarkdownToJSX.Options = { Row, // Our own components + Btn, I18nLink, I18nBtnChangeLocale, Tooltip, @@ -69,12 +90,17 @@ const defaultMarkdownOptions: MarkdownToJSX.Options = { }; /** - * Display "text" property as Markdown, using the "markdown-to-jsx" library + * Display the "text" property as Markdown, using the "markdown-to-jsx" library. * - * @param props + * @see https://github.com/probablyup/markdown-to-jsx */ const Markdown: React.FunctionComponent = (props): JSX.Element => { - const { text, markdownOptions: _markdownOptions, style, className } = props; + const { + text, + markdownOptions: _markdownOptions, + style, + className, + } = props; const markdownOptions = deepmerge(defaultMarkdownOptions, _markdownOptions || {}); // If providing a non-string input (like "null" or "undefined") then markdown-to-jsx will crash with "Cannot read property 'replace' of undefined" - See https://github.com/probablyup/markdown-to-jsx/issues/314 diff --git a/src/components/utils/SimpleTooltip.tsx b/src/components/utils/SimpleTooltip.tsx index bee94e62d..a6d0b6f45 100644 --- a/src/components/utils/SimpleTooltip.tsx +++ b/src/components/utils/SimpleTooltip.tsx @@ -1,14 +1,30 @@ +import { TooltipProps } from 'rc-tooltip/lib/Tooltip'; import React from 'react'; -import Tooltip from './Tooltip'; +import Tooltip, { TooltipPlacement } from './Tooltip'; -type Props = { +export type Props = { + /** + * React children, usually text. + */ children: React.ReactElement; - placement?: string; + + /** + * Tooltip's placement. + * + * @default top + */ + placement?: TooltipPlacement; + + /** + * Tooltip's text. + * + * Displayed as an overlay within a `
` element. + */ text: string; -} +} & Partial; /** - * Tooltip with simplified props meant to be used from Markdown + * Tooltip with simplified props meant to be used from Markdown. * * @example Click me */ diff --git a/src/components/utils/SpoilerButton.tsx b/src/components/utils/SpoilerLink.tsx similarity index 54% rename from src/components/utils/SpoilerButton.tsx rename to src/components/utils/SpoilerLink.tsx index 781a9bd3a..9c5db3c14 100644 --- a/src/components/utils/SpoilerButton.tsx +++ b/src/components/utils/SpoilerLink.tsx @@ -1,12 +1,45 @@ import classnames from 'classnames'; import React, { useState } from 'react'; -type Props = { +export type Props = { + /** + * Element displayed within a `span`, until it is clicked. + * + * Is hidden once clicked. + */ previewElement: JSX.Element; + + /** + * Element displayed within a `Link` once the preview element has been clicked. + * + * Redirects to another web page, or can open the email/phone using the browser's native feature. + * + * Doesn't have built-in analytics event support. Those need to be implemented by the caller. + */ spoilerElement: JSX.Element; + + /** + * CSS classes. + */ className?: string; + + /** + * The path or URL to navigate to. + * + * Usually something like "tel:XXXXX" or "mailto:john.doe@somewhere.com" + */ href: string; + + /** + * Link target. + * + * @default _blank + */ target?: string; + + /** + * HTML id attribute. Must be unique. + */ id?: string; } @@ -14,16 +47,9 @@ type Props = { * Displays a preview element until the preview is clicked, then displays the spoiler element. * Meant to be used to hide some information until the preview is clicked. * - * The spoiler element is a link, which redirects to another web page, or can open the email/phone native. - * Doesn't have built-in analytics event support. Those need to be implemented by the caller. - * - * XXX Defining the "key" attributes will ensure each instance is treated completely separately and won't share its state with another instance. - * - * The elements types are hardcoded: - * - previewElement is a clickable element - * - spoilerElement is a clickable element + * > XXX Defining the "key" attributes will ensure each instance is treated completely separately and won't share its state with another instance. */ -const SpoilerButton: React.FunctionComponent = (props): JSX.Element => { +const SpoilerLink: React.FunctionComponent = (props): JSX.Element => { const { previewElement, spoilerElement, @@ -59,4 +85,4 @@ const SpoilerButton: React.FunctionComponent = (props): JSX.Element => { } }; -export default SpoilerButton; +export default SpoilerLink; diff --git a/src/components/utils/Stamp.tsx b/src/components/utils/Stamp.tsx index 18961c4c7..788a83ca3 100644 --- a/src/components/utils/Stamp.tsx +++ b/src/components/utils/Stamp.tsx @@ -1,9 +1,13 @@ -import { css } from '@emotion/react'; -import { useTheme } from '@emotion/react'; +import { + css, + useTheme, +} from '@emotion/react'; import React, { ReactNode } from 'react'; -import { CustomerTheme } from '../../types/data/CustomerTheme'; -type Props = { +export type Props = { + /** + * React children, usually text. + */ children: ReactNode; } @@ -17,14 +21,17 @@ export const Stamp: React.FunctionComponent = (props): JSX.Element => { children, ...rest } = props; - const { secondaryColorVariant1, secondaryColor } = useTheme(); + const { + secondaryColorVariant1, + secondaryColor, + } = useTheme(); return (
= (props): JSX.Element => { } } - .stamp-text{ + .stamp-text { margin-top: 4px; display: contents; } - `} + `} {...rest} >
diff --git a/src/components/utils/Text.tsx b/src/components/utils/Text.tsx index dff154f45..14778ca92 100644 --- a/src/components/utils/Text.tsx +++ b/src/components/utils/Text.tsx @@ -1,15 +1,24 @@ -import React from 'react'; +import React, { DOMAttributes } from 'react'; -type Props = { +export type Props = { + /** + * React children, usually text. + */ children: string; - tag?: string | React.ReactType; -} + + /** + * HTML tag. (`span`, `div`, etc.) + * + * @default div + */ + tag?: string | React.ElementType; +} & DOMAttributes; /** - * Automatically break lines for text - * Allow usage of HTML (but not React components) + * Automatically break lines for text. + * Allow usage of HTML (but not React components). * - * Avoids relying on
for every line break + * Avoids relying on <br /> for every line break. * * @example * @@ -19,11 +28,12 @@ type Props = { * Another line, which will respect line break * `} * - * - * @param props */ export const Text: React.FunctionComponent = (props) => { - const { children, tag: Wrapper = 'div' } = props; + const { + children, + tag: Wrapper = 'div', + } = props; return ( XXX The component will not be interactive without a unique id! + */ id: string; - mode?: 'light' | 'ios' | 'skewed' | 'flat' | 'flip'; + + /** + * Display mode. + * + * @default flip + */ + mode?: 'flat' | 'flip' | 'ios' | 'light' | 'skewed'; + + /** + * Options for `flip` display mode. + * + * @default { useBackgroundColor: true } + */ flipModeOptions?: { useBackgroundColor?: boolean; }; - valueOn: any; - valueOff: any; + + /** + * Content to display when the toggle is "on" (`checked = true`). + */ + contentOn: any; + + /** + * Content to display when the toggle is "off" (`checked = false`). + */ + contentOff: any; + + /** + * Whether the toggle is checked. + * + * Should be a controlled property. + */ isChecked?: boolean; } & React.HTMLProps; /** * Toggle button (as checkbox) between two possible values. * - * @param props + * Provides several display modes. + * * @see https://codepen.io/mallendeo/pen/eLIiG */ const ToggleButton: React.FunctionComponent = (props): JSX.Element => { const { id, - mode = 'flip', + mode = 'flat', flipModeOptions = { - useBackgroundColor: false, + useBackgroundColor: true, }, - valueOn, - valueOff, + contentOn, + contentOff, isChecked, ...rest } = props; return ( = (props): JSX.Element => { &:after { left: 100%; - content: ${valueOn}; + content: ${contentOn}; } &:before { left: 0; - content: ${valueOff}; + content: ${contentOff}; } &:active { @@ -254,14 +287,14 @@ const ToggleButton: React.FunctionComponent = (props): JSX.Element => { } &:after { - content: ${valueOn}; + content: ${contentOn}; background: ${flipModeOptions?.useBackgroundColor ? '#02C66F' : 'initial'}; transform: rotateY(-180deg); } &:before { background: ${flipModeOptions?.useBackgroundColor ? '#FF3A19' : 'initial'}; - content: ${valueOff}; + content: ${contentOff}; } &:active:before { diff --git a/src/components/utils/ToggleLanguagesButton.tsx b/src/components/utils/ToggleLanguagesButton.tsx index 8db1cc288..80f44e4e3 100644 --- a/src/components/utils/ToggleLanguagesButton.tsx +++ b/src/components/utils/ToggleLanguagesButton.tsx @@ -1,20 +1,38 @@ import React from 'react'; import ToggleButton from './ToggleButton'; -type Props = { +export type Props = { + /** + * HTML id attribute. Must be unique. + * + * XXX The component will not be interactive without a unique id! + */ id: string; - flag1?: any; // Expects image used as background. E.g: `url("data:image/svg+xml, ...")` - flag2?: any; // Expects image used as background. E.g: `url("data:image/svg+xml, ...")` + + /** + * Flag used when the toggle button is checked. + */ + flagOn?: string; + + /** + * Flag used when the toggle button is not checked. + */ + flagOff?: string; + + /** + * Whether the toggle is checked. + * + * Should be a controlled property. + */ isChecked?: boolean; } & React.HTMLProps; /** * Button that toggles between two language flags. * - * Use a SVG encoder for SVGs - * - https://yoksel.github.io/url-encoder/ Copy the SVG code and take the "Ready for CSS" value (without the "background-color" part) + * Use a SVG encoder for SVGs: * - * @param props + * - https://yoksel.github.io/url-encoder/ Copy the SVG code and take the "Ready for CSS" value (without the "background-color" part) */ const ToggleLanguagesButton: React.FunctionComponent = (props): JSX.Element => { const frenchFlagSvg = `url("data:image/svg+xml,%3Csvg id='Calque_1' data-name='Calque 1' xmlns='http://www.w3.org/2000/svg' width='24.43' height='12.91' viewBox='0 0 24.43 12.91'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23012169;%7D.cls-2%7Bfill:%23c8102e;%7D%3C/style%3E%3C/defs%3E%3Crect class='cls-1' width='7.98' height='12.91'/%3E%3Crect class='cls-2' x='16.45' width='7.98' height='12.91'/%3E%3C/svg%3E")`; @@ -23,18 +41,23 @@ const ToggleLanguagesButton: React.FunctionComponent = (props): JSX.Eleme const englishHybridFlagSvg = `url("data:image/svg+xml,%3Csvg id='Calque_1' data-name='Calque 1' xmlns='http://www.w3.org/2000/svg' width='24.43' height='12.91' viewBox='0 0 24.39 12.88'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23bf2333;%7D.cls-2,.cls-4%7Bfill:%23fff;%7D.cls-3%7Bfill:%23223164;%7D.cls-4%7Bfill-rule:evenodd;%7D.cls-5%7Bfill:%23283575;%7D.cls-6%7Bfill:%23d0232e;%7D%3C/style%3E%3C/defs%3E%3Crect id='rect8767' class='cls-1' x='0.03' y='11.75' width='24.26' height='1.03'/%3E%3Crect id='rect8769' class='cls-2' x='0.03' y='10.72' width='24.26' height='1.03'/%3E%3Crect id='rect8771' class='cls-1' x='0.03' y='9.7' width='24.26' height='1.03'/%3E%3Crect id='rect8773' class='cls-2' x='0.03' y='8.68' width='24.26' height='1.03'/%3E%3Crect id='rect8775' class='cls-1' x='0.03' y='7.72' width='24.26' height='0.96'/%3E%3Crect id='rect8777' class='cls-2' x='0.03' y='6.76' width='24.26' height='0.96'/%3E%3Crect id='rect8779' class='cls-1' x='0.03' y='5.79' width='24.26' height='0.96'/%3E%3Crect id='rect8781' class='cls-2' x='0.03' y='4.83' width='24.26' height='0.96'/%3E%3Crect id='rect8783' class='cls-1' x='0.03' y='3.87' width='24.26' height='0.96'/%3E%3Crect id='rect8785' class='cls-2' x='0.03' y='2.92' width='24.26' height='0.96'/%3E%3Crect id='rect8787' class='cls-1' x='0.03' y='1.96' width='24.26' height='0.96'/%3E%3Crect id='rect8791' class='cls-2' x='0.03' y='0.99' width='24.26' height='0.96'/%3E%3Crect id='rect8793' class='cls-1' x='0.03' y='0.03' width='24.26' height='0.96'/%3E%3Crect id='rect8796' class='cls-3' x='0.03' y='0.03' width='9.73' height='6.72'/%3E%3Cpath id='path8825' class='cls-4' d='M.44,6H.72l.11-.28L.94,6h.28L1,6.14l.07.29L.83,6.28l-.24.16.06-.29Z'/%3E%3Cpath id='path8827' class='cls-4' d='M2,6h.28l.11-.28L2.54,6h.29l-.21.19.06.29-.24-.15-.23.16.06-.29Z'/%3E%3Cpath id='path8829' class='cls-4' d='M3.7,6H4l.11-.28L4.2,6h.29l-.22.19.07.29-.24-.2-.25.16.06-.29Z'/%3E%3Cpath id='path8831' class='cls-4' d='M5.3,6h.28l.11-.28L5.81,6h.28l-.21.19.06.29-.24-.2-.24.16.06-.29Z'/%3E%3Cpath id='path8833' class='cls-4' d='M6.9,6h.29l.1-.28L7.41,6h.28l-.21.19.07.29-.25-.2-.24.16.06-.29Z'/%3E%3Cpath id='path8835' class='cls-4' d='M8.57,6h.28L9,5.68,9.08,6h.27l-.16.19v.3L9,6.29l-.24.16.06-.29Z'/%3E%3Cpath id='path8837' class='cls-4' d='M1.27,5.32h.28L1.66,5l.12.27h.28l-.21.2.06.29-.24-.15-.24.16.06-.29Z'/%3E%3Cpath id='path8839' class='cls-4' d='M.44,4.61H.72l.11-.28.11.28h.28L1,4.81l.07.29L.83,5l-.24.1.06-.29Z'/%3E%3Cpath id='path8841' class='cls-4' d='M1.27,4h.28l.11-.28L1.78,4h.28l-.21.2.06.29-.24-.15-.24.15.06-.29Z'/%3E%3Cpath id='path8843' class='cls-4' d='M.44,3.26H.72L.83,3l.11.26h.28L1,3.46l.07.29L.83,3.6l-.24.16.06-.29Z'/%3E%3Cpath id='path8845' class='cls-4' d='M1.27,2.63h.28l.11-.27.12.27h.28l-.21.2.06.28L1.67,3l-.24.15.06-.29Z'/%3E%3Cpath id='path8847' class='cls-4' d='M.44,1.93H.72l.11-.28.11.27h.28L1,2.11l.07.29L.83,2.25l-.24.16.06-.29Z'/%3E%3Cpath id='path8849' class='cls-4' d='M1.27,1.29h.28L1.66,1l.12.27h.28l-.21.2.06.29-.24-.15-.24.16.06-.29Z'/%3E%3Cpath id='path8851' class='cls-4' d='M.44.58H.72L.83.3.94.58h.28L1,.78l.07.29L.83.92l-.24.15L.65.78Z'/%3E%3Cpath id='path8853' class='cls-4' d='M2.87,5.32h.28L3.25,5l.12.27h.28l-.21.2.07.29-.25-.15L3,5.81l0-.29Z'/%3E%3Cpath id='path8855' class='cls-4' d='M2,4.61h.28l.11-.28.11.28h.29l-.21.2.06.29L2.44,5l-.24.1.06-.29Z'/%3E%3Cpath id='path8857' class='cls-4' d='M2.87,4h.28l.1-.28L3.37,4h.28l-.21.2.07.29-.25-.15L3,4.46l0-.29Z'/%3E%3Cpath id='path8859' class='cls-4' d='M2,3.26h.28L2.43,3l.11.26h.29l-.21.2.06.29L2.44,3.6l-.24.16.06-.29Z'/%3E%3Cpath id='path8861' class='cls-4' d='M2.87,2.63h.28l.1-.27.12.27h.28l-.21.2.07.28L3.26,3,3,3.12l0-.29Z'/%3E%3Cpath id='path8863' class='cls-4' d='M2,1.93h.28l.11-.28.11.27h.29l-.21.19.06.29L2.4,2.25l-.23.2.06-.29Z'/%3E%3Cpath id='path8865' class='cls-4' d='M2.87,1.29h.28L3.25,1l.12.27h.28l-.21.2.07.29-.25-.15L3,1.78l0-.29Z'/%3E%3Cpath id='path8867' class='cls-4' d='M2,.58h.28L2.43.3l.11.28h.29l-.21.2.06.29L2.44.92l-.24.15L2.26.78Z'/%3E%3Cpath id='path8869' class='cls-4' d='M4.47,5.32h.28L4.86,5,5,5.31h.28L5,5.51l.07.29-.25-.15-.24.16.06-.29Z'/%3E%3Cpath id='path8871' class='cls-4' d='M3.7,4.61H4l.11-.28.11.28h.29l-.22.2.07.29L4.1,5l-.25.15.06-.29Z'/%3E%3Cpath id='path8873' class='cls-4' d='M6.9,4.61h.29l.1-.28.12.28h.28l-.21.2.07.29L7.3,5l-.24.15.06-.29Z'/%3E%3Cpath id='path8875' class='cls-4' d='M5.3,4.61h.28l.11-.28.12.28h.28l-.21.2.06.29L5.7,5l-.24.15.06-.29Z'/%3E%3Cpath id='path8877' class='cls-4' d='M7.74,5.32H8L8.13,5l.11.27h.29l-.22.2.07.29-.24-.15-.25.16L8,5.52Z'/%3E%3Cpath id='path8879' class='cls-4' d='M6.13,5.32h.29L6.52,5l.12.27h.28l-.21.2.07.29-.25-.15-.24.16.06-.29Z'/%3E%3Cpath id='path8881' class='cls-4' d='M3.7.58H4L4.09.3,4.2.58h.29l-.22.2.07.29L4.1.92l-.25.15L3.91.78Z'/%3E%3Cpath id='path8883' class='cls-4' d='M4.47,1.29h.28L4.86,1,5,1.28h.28L5,1.48l.07.29-.25-.15-.24.16.06-.29Z'/%3E%3Cpath id='path8885' class='cls-4' d='M3.7,1.93H4l.11-.28.11.27h.29l-.22.19.07.29L4.1,2.25l-.25.16.06-.29Z'/%3E%3Cpath id='path8887' class='cls-4' d='M4.47,2.63h.28l.11-.27L5,2.63h.28L5,2.83l.07.28L4.86,3l-.24.15.06-.29Z'/%3E%3Cpath id='path8889' class='cls-4' d='M3.7,3.26H4L4.09,3l.11.26h.29l-.22.2.07.29L4.1,3.6l-.25.16.06-.29Z'/%3E%3Cpath id='path8891' class='cls-4' d='M4.47,4h.28l.11-.28L5,4h.28L5,4.17l.07.29-.25-.15-.24.15.06-.29Z'/%3E%3Cpath id='path8893' class='cls-4' d='M8.57,4.61h.28L9,4.33l.12.28h.27l-.2.2V5.1L9,5l-.24.15.06-.29Z'/%3E%3Cpath id='path8895' class='cls-4' d='M5.3,3.26h.28L5.69,3l.12.26h.28l-.21.2.06.29L5.7,3.6l-.24.16.06-.29Z'/%3E%3Cpath id='path8897' class='cls-4' d='M5.3.58h.28L5.69.3l.12.28h.28l-.21.2.06.29L5.7.92l-.24.15L5.52.78Z'/%3E%3Cpath id='path8899' class='cls-4' d='M5.3,1.93h.28l.11-.28.12.27h.28l-.21.19.06.29L5.7,2.25l-.24.16.06-.29Z'/%3E%3Cpath id='path8901' class='cls-4' d='M7.74,4H8l.11-.28L8.24,4h.29l-.22.2.07.29-.24-.15-.25.15L8,4.17Z'/%3E%3Cpath id='path8903' class='cls-4' d='M6.13,4h.29l.1-.28L6.64,4h.28l-.21.2.07.29-.25-.15-.24.15.06-.29Z'/%3E%3Cpath id='path8905' class='cls-4' d='M6.13,1.29h.29L6.52,1l.12.27h.28l-.21.2.07.29-.25-.15-.24.16.06-.29Z'/%3E%3Cpath id='path8907' class='cls-4' d='M6.13,2.63h.29l.1-.27.12.27h.28l-.21.2.07.28L6.53,3l-.24.15.06-.29Z'/%3E%3Cpath id='path8909' class='cls-4' d='M8.57,3.26h.28L9,3l.12.26h.27l-.16.18.06.29L9,3.58l-.24.16.06-.29Z'/%3E%3Cpath id='path8911' class='cls-4' d='M6.9,3.26h.29L7.29,3l.12.26h.28l-.21.2.07.29L7.3,3.6l-.24.16.06-.29Z'/%3E%3Cpath id='path8913' class='cls-4' d='M6.9.58h.29L7.29.3l.12.28h.28l-.21.2.07.29L7.3.92l-.24.15L7.12.78Z'/%3E%3Cpath id='path8915' class='cls-4' d='M6.9,1.93h.29l.1-.28.12.27h.28l-.21.19.07.29L7.3,2.25l-.24.16.06-.29Z'/%3E%3Cpath id='path8917' class='cls-4' d='M8.57.58h.28L9,.3l.12.28h.27l-.2.2v.29L9,.92l-.24.15,0-.29Z'/%3E%3Cpath id='path8919' class='cls-4' d='M7.74,1.29H8L8.13,1l.11.27h.29l-.22.2.07.29-.24-.15-.25.16L8,1.49Z'/%3E%3Cpath id='path8921' class='cls-4' d='M8.57,1.93h.28L9,1.65l.12.27h.27l-.2.19v.34L9,2.3l-.24.16.06-.29Z'/%3E%3Cpath id='path8923' class='cls-4' d='M7.74,2.63H8l.11-.27.11.27h.29l-.22.2.07.28L8.14,3l-.25.15L8,2.83Z'/%3E%3Cpath id='path3835' class='cls-2' d='M0,12.8,24.39.08V12.83Z'/%3E%3Cpath id='path3839' class='cls-5' d='M14.19,12.78V8.13h10.1v4.63Z'/%3E%3Cpath id='path3843' class='cls-5' d='M24.29.05,16,4.44h8.25Z'/%3E%3Cpath id='path3845' class='cls-5' d='M0,12.77l9.1-4.7h1.25v4.72Z'/%3E%3Cpath id='path3847' class='cls-2' d='M24.35,11.5v1.38H22.17L13.85,8.52l.09-.61,3.69.1Z'/%3E%3Cpath id='path3855' class='cls-2' d='M24.37,0,15.82,4.54l2.65.06,5.85-3.15Z'/%3E%3Cpath id='path3859' class='cls-2' d='M0,12.77,9.51,7.85h1v.93l-7.46,4Z'/%3E%3Cpath id='path4355' class='cls-6' d='M0,12.77,9.36,7.92h1v.5L2.08,12.76Z'/%3E%3Cpath id='path4357' class='cls-6' d='M15.27,8l1.84.05,7.2,3.85v.84Z'/%3E%3Cpath id='path3058' class='cls-6' d='M24.29,7.44h-14L15,5.06h9.36Z'/%3E%3Cpath id='path3060' class='cls-6' d='M13.42,12.74V5.85L11,7.08v5.69Z'/%3E%3C/svg%3E")`; const { id, - flag1 = frenchFlagSvg, - flag2 = englishHybridFlagSvg, + flagOn = frenchFlagSvg, + flagOff = englishHybridFlagSvg, isChecked, ...rest } = props; return ( + // @ts-ignore ); diff --git a/src/components/utils/Tooltip.tsx b/src/components/utils/Tooltip.tsx index 45f10e286..ed41bd2ed 100644 --- a/src/components/utils/Tooltip.tsx +++ b/src/components/utils/Tooltip.tsx @@ -1,21 +1,46 @@ import RCTooltip from 'rc-tooltip'; +import { TooltipProps } from 'rc-tooltip/lib/Tooltip'; +import { ActionType } from 'rc-trigger/lib/interface'; import React from 'react'; -type Props = { +export type TooltipPlacement = 'left' | 'right' | 'top' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; + +export type Props = { + /** + * React children, usually text. + */ children: React.ReactElement; - overlay: React.ReactElement; - trigger?: Array; - placement?: string; - visible?: boolean; -} + + /** + * Component that will be displayed as the tooltip. + * + * Usually, something like `Tooltip content`. + * + * XXX If the content isn't placed in an element _(e.g: Fragment)_, [the whole React app will crash](https://github.com/react-component/tooltip/issues/239)! + */ + overlay: (() => React.ReactNode) | React.ReactNode; + + /** + * Triggers defining when the tooltip displays. + * + * @default ['hover', 'click', 'focus'] + */ + trigger?: ActionType | ActionType[]; + + /** + * Tooltip's placement. + * + * @default top + */ + placement?: TooltipPlacement; +} & TooltipProps; /** - * Tooltip with sane defaults that improve usability and accessibility. + * Tooltip override, improving developer experience and end-user accessibility by default. * - * Uses React Component Tooltip (https://github.com/react-component/tooltip) - * XXX Feel free to add more API options, I've only added what seemed necessary but they support plenty more! + * Uses [React Component Tooltip](https://github.com/react-component/tooltip) * - * @param {Props} props + * > Only the most useful properties are being described, [more are available](https://github.com/react-component/tooltip#api). */ const Tooltip: React.FunctionComponent = (props): JSX.Element => { const { diff --git a/src/hocs/withHOCTemplate.tsx b/src/hocs/withHOCTemplate.tsx index 17a2d48ef..b396123e0 100644 --- a/src/hocs/withHOCTemplate.tsx +++ b/src/hocs/withHOCTemplate.tsx @@ -1,14 +1,25 @@ import React from 'react'; import { wrapDisplayName } from 'recompose'; -// Props you want the resulting component to take (besides the props of the wrapped component) -type ExternalProps = {} +/** + * Props you want the resulting component to take (besides the props of the wrapped component). + */ +type ExternalProps = {}; -// Props the HOC adds to the wrapped component -export type InjectedProps = {} +/** + * Props the HOC adds to the wrapped component. + */ +export type InjectedProps = {}; -// Options for the HOC factory that are not dependent on props values -type Options = {} +/** + * Options for the HOC factory that are not dependent on props values. + */ +type Options = {}; + +/** + * Options applied by default. + */ +const defaultOptions: Partial = {}; /** * HOC template meant to be duplicated to build your custom HOC @@ -16,7 +27,7 @@ type Options = {} * * @see Inspired from https://gist.github.com/rosskevin/6c103846237ecbc77862ea0f3218187d */ -const withHOCTemplate = ({}: Options = {}) => ( +const withHOCTemplate = ({}: Options = defaultOptions as Options) => ( WrappedComponent: React.ComponentType, ): React.ComponentClass => { class WithHOCTemplate extends React.Component { diff --git a/src/i18nConfig.js b/src/i18nConfig.js index dbfd2cfef..664b1b8c0 100644 --- a/src/i18nConfig.js +++ b/src/i18nConfig.js @@ -3,19 +3,63 @@ Note that any change should/must be followed by a server restart, because it's used in "next.config.js" */ +/** + * Select the "supportedLocales.name" you want to use by default in your app. + * This value will be used as a fallback value, when the user locale cannot be resolved. + * + * @example en + * @example en-US + * + * @type {string} + */ const defaultLocale = 'en'; + +/** + * List of all supported locales by your app. + * + * If a user tries to load your site using non-supported locales, the default locale is used instead. + * + * @type {({name: string, lang: string}|{name: string, lang: string}|{name: string, lang: string})[]} + */ const supportedLocales = [ - { name: 'fr', lang: 'fr' }, - { name: 'en-US', lang: 'en' }, - { name: 'en', lang: 'en' }, + { + name: 'fr', + lang: 'fr', + }, + { + name: 'en-US', + lang: 'en', + }, + { + name: 'en', + lang: 'en', + }, ]; + +/** + * Returns the list of all supported languages. + * Basically extracts the "lang" parameter from the supported locales array. + * + * @type {string[]} + */ const supportedLanguages = supportedLocales.map((item) => { return item.lang; }); +/** + * Resolves the lang associated to a locale. + * + * @param localeToFind + * @return {string} + */ +const getLangFromLocale = (localeToFind) => { + return (supportedLocales.find((locale) => locale.name === localeToFind)).name; +}; + // XXX Available through utils/i18n/i18n module.exports = { - defaultLocale: defaultLocale, - supportedLocales: supportedLocales, + defaultLocale, + supportedLocales, supportedLanguages: [...new Set(supportedLanguages)], // Remove duplicates + getLangFromLocale, }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f351949e7..b4ce673fe 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,11 +1,8 @@ -import 'animate.css/animate.min.css'; // Loads animate.css CSS file. See https://github.com/daneden/animate.css -import 'bootstrap/dist/css/bootstrap.min.css'; // Loads bootstrap CSS file. See https://stackoverflow.com/a/50002905/2391795 -import 'cookieconsent/build/cookieconsent.min.css'; // Loads CookieConsent CSS file. See https://github.com/osano/cookieconsent import size from 'lodash.size'; -import 'rc-tooltip/assets/bootstrap.css'; import React from 'react'; import { v1 as uuid } from 'uuid'; // XXX Use v1 for uniqueness - See https://www.sohamkamani.com/blog/2016/10/05/uuid1-vs-uuid4/ import MultiversalAppBootstrap from '../components/appBootstrap/MultiversalAppBootstrap'; +import '../components/appBootstrap/MultiversalGlobalExternalStyles'; import { MultiversalAppBootstrapProps } from '../types/nextjs/MultiversalAppBootstrapProps'; import { NextWebVitalsMetrics } from '../types/nextjs/NextWebVitalsMetrics'; import { NextWebVitalsMetricsReport } from '../types/nextjs/NextWebVitalsMetricsReport'; diff --git a/src/stories/app/README.stories.mdx b/src/stories/app/README.stories.mdx new file mode 100644 index 000000000..783394c25 --- /dev/null +++ b/src/stories/app/README.stories.mdx @@ -0,0 +1,49 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Getting started + +> The documentation regarding Storybook is hosted on this Storybook demo, not on the [Documentation site](https://unlyed.github.io/next-right-now). + +
+ +--- + +
Custom components
+ +Next Right Now doesn't enforce a particular way to write your components **stories**, but we've got some advices: +- `src/stories/sb-examples` contains the default Storybook examples. They contain useful links and resources you should probably keep around. +- `src/stories/nrn` contains Next Right Now components. Feel free to delete those you don't need them! We suggest keeping the Storybook documentation for those you use. +- `src/stories/app` is for you, you can rename it, split it into several folder, do as you want. + +> We suggest not renaming Next Right Now and Storybook existing stories if you plan on using Next Right Now as a boilerplate, as it would make sync with NRN harder. + +
Folders and stories
+ +Folders don't dictate in which section your stories will be displayed. + +> You can have different folders within `src/stories` publishing stories in the same section, it depends on the Story `title`, not its file path. + +Therefore, you could mix NRN native components with your own (put them in the same section but in different folders), if you'd like to do it this way. + +
Storybook configuration documentation
+ +Many efforts have been put into Storybook configuration documentation. + +The best place to start discovering what Next Right Now has done for you are: +- `.storybook/main.js`: Describes all the addons being installed and why. With links to their respective documentation. +- `storybook/manager.js`: Describes the Storybook UI customization. +- `storybook/preview.js`: Describes the Storybook "component preview" configuration. +It is the "story preview entrypoint" and contains the whole Preview configuration. + - It's where you can customise the `storySort`, to manipulate the ordering of the stories. + +--- + +
+ TipEdit the Markdown in{' '} + src/stories/app/README.stories.mdx +
diff --git a/src/stories/nrn/Welcome.stories.mdx b/src/stories/nrn/Welcome.stories.mdx new file mode 100644 index 000000000..12afe571a --- /dev/null +++ b/src/stories/nrn/Welcome.stories.mdx @@ -0,0 +1,53 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import Code from '../shared/assets/code-brackets.svg'; +import Comments from '../shared/assets/comments.svg'; +import Direction from '../shared/assets/direction.svg'; +import Repo from '../shared/assets/repo.svg'; + + + +# Welcome to Next Right Now - Storybook + +Next Right Now includes a Storybook project **built-in**. + +This section is about how Storybook is configured with Next Right Now. + +> If you want to see the default Storybook example [come here!](/?path=/story/storybook-example-introduction--page) + +
Learn how Next Right Now works
+ + + +
+ TipEdit the Markdown in{' '} + src/stories/nrn/Welcome.stories.mdx +
diff --git a/src/stories/nrn/a11y/example.stories.tsx b/src/stories/nrn/a11y/example.stories.tsx new file mode 100644 index 000000000..2d519f99a --- /dev/null +++ b/src/stories/nrn/a11y/example.stories.tsx @@ -0,0 +1,22 @@ +import { Meta } from '@storybook/react/types-6-0'; +import React from 'react'; + +export default { + title: 'Next Right Now/A11y/Button (default example)', + parameters: { + controls: { + hideNoControlsWarning: true, + }, + }, +} as Meta; + +export const accessible = () => ; + +export const inaccessible = () => ( + +); diff --git a/src/stories/nrn/animation/Animated3Dots.stories.tsx b/src/stories/nrn/animation/Animated3Dots.stories.tsx new file mode 100644 index 000000000..f9256819f --- /dev/null +++ b/src/stories/nrn/animation/Animated3Dots.stories.tsx @@ -0,0 +1,21 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Animated3Dots, { Props } from '../../../components/svg/Animated3Dots'; + +export default { + title: 'Next Right Now/Animation/Animated3Dots', + component: Animated3Dots, + argTypes: { + fill: { + control: 'color', + }, + }, +} as Meta; + +export const DefaultExample: Story = (props) => ; +DefaultExample.args = { + fill: 'blue', +}; diff --git a/src/stories/nrn/animation/AnimatedTextBubble.stories.tsx b/src/stories/nrn/animation/AnimatedTextBubble.stories.tsx new file mode 100644 index 000000000..1d314243a --- /dev/null +++ b/src/stories/nrn/animation/AnimatedTextBubble.stories.tsx @@ -0,0 +1,21 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import AnimatedTextBubble, { Props } from '../../../components/svg/AnimatedTextBubble'; + +export default { + title: 'Next Right Now/Animation/AnimatedTextBubble', + component: AnimatedTextBubble, + argTypes: { + fill: { + control: 'color', + }, + }, +} as Meta; + +export const DefaultExample: Story = (props) => ; +DefaultExample.args = { + fill: 'blue', +}; diff --git a/src/stories/nrn/animation/BubbleTimer.stories.tsx b/src/stories/nrn/animation/BubbleTimer.stories.tsx new file mode 100644 index 000000000..4a3bf1168 --- /dev/null +++ b/src/stories/nrn/animation/BubbleTimer.stories.tsx @@ -0,0 +1,28 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import BubbleTimer, { Props } from '../../../components/utils/BubbleTimer'; + +export default { + title: 'Next Right Now/Animation/BubbleTimer', + component: BubbleTimer, + argTypes: {}, +} as Meta; + +export const DefaultExample: Story = (props) => { + const { duration } = props; + + return ( + +

Content displayed after {duration}ms has passed.

+
+ ); +}; +DefaultExample.args = { + fill: 'blue', + duration: 2000, +}; diff --git a/src/stories/nrn/animation/Loader.stories.tsx b/src/stories/nrn/animation/Loader.stories.tsx new file mode 100644 index 000000000..ede3981d7 --- /dev/null +++ b/src/stories/nrn/animation/Loader.stories.tsx @@ -0,0 +1,18 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Loader, { Props } from '../../../components/animations/Loader'; + +export default { + title: 'Next Right Now/Animation/Loader', + component: Loader, + parameters: { + controls: { + hideNoControlsWarning: true, + }, + }, +} as Meta; + +export const DefaultExample: Story = () => ; diff --git a/src/stories/nrn/asset/AirtableAsset.stories.tsx b/src/stories/nrn/asset/AirtableAsset.stories.tsx new file mode 100644 index 000000000..1153383df --- /dev/null +++ b/src/stories/nrn/asset/AirtableAsset.stories.tsx @@ -0,0 +1,39 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import AirtableAsset, { Props } from '../../../components/assets/AirtableAsset'; +import { Asset } from '../../../types/data/Asset'; + +const defaultLogoUrl = 'https://dl.airtable.com/lA5gmGBQheUvmuX616wU_monochromelogo.png'; + +export default { + title: 'Next Right Now/Asset/AirtableAsset', + component: AirtableAsset, + argTypes: {}, +} as Meta; + +export const DynamicAirtableLogo: Story = (props) => { + return ( + + ); +}; +DynamicAirtableLogo.args = { + id: 'default-logo', + className: 'default-class', + asset: { + url: defaultLogoUrl, + linkUrl: 'https://github.com/UnlyEd/next-right-now', + } as Asset, + transformationsOverride: { + width: 300, + height: 100, + }, + style: { + backgroundColor: 'white', + }, + onClick: () => console.log('click on asset'), +}; diff --git a/src/stories/nrn/dataDisplay/Btn.stories.tsx b/src/stories/nrn/dataDisplay/Btn.stories.tsx new file mode 100644 index 000000000..9b8d982c8 --- /dev/null +++ b/src/stories/nrn/dataDisplay/Btn.stories.tsx @@ -0,0 +1,194 @@ +import { css } from '@emotion/react'; +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Btn, { Props } from '../../../components/utils/Btn'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string; +}; + +export default { + title: 'Next Right Now/Data display/Btn', + component: Btn, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { text } = props; + + return ( + // @ts-ignore + console.info('Click')} + > + {text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: 'Hello', +}; + +export const Buttons: Story = () => { + return ( +
+
+     + + Default + +     + + Default isTransparent + +     + + Reverse + +     + + Reverse isTransparent + +     + + Outline + +     + + Outline isTransparent + +     + + Variant + +
+ +
+     + + Secondary + +     + + Secondary isTransparent + +     + + Secondary Reverse + +     + + Secondary Reverse isTransparent + +     + + Secondary Outline + +     + + Secondary Outline isTransparent + +     + + Secondary Variant + +
+ +
+
+
+ +
+     + + Default + +     + + Default isTransparent + +     + + Reverse + +     + + Reverse isTransparent + +     + + Outline + +     + + Outline isTransparent + +     + + Variant + +
+ +
+     + + Secondary + +     + + Secondary isTransparent + +     + + Secondary Reverse + +     + + Secondary Reverse isTransparent + +     + + Secondary Outline + +     + + Secondary Outline isTransparent + +     + + Secondary Variant + +
+
+ ); +}; +Buttons.parameters = { + controls: { + hideNoControlsWarning: true, + }, +}; diff --git a/src/stories/nrn/dataDisplay/CircleBtn.stories.tsx b/src/stories/nrn/dataDisplay/CircleBtn.stories.tsx new file mode 100644 index 000000000..2826bba87 --- /dev/null +++ b/src/stories/nrn/dataDisplay/CircleBtn.stories.tsx @@ -0,0 +1,36 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import CircleBtn, { Props } from '../../../components/utils/CircleBtn'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string; +}; + +export default { + title: 'Next Right Now/Data display/CircleBtn', + component: CircleBtn, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { text } = props; + + return ( + // @ts-ignore + console.info('Click')} + > + {text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: '42', +}; diff --git a/src/stories/nrn/dataDisplay/Code.stories.tsx b/src/stories/nrn/dataDisplay/Code.stories.tsx new file mode 100644 index 000000000..1ecac6c28 --- /dev/null +++ b/src/stories/nrn/dataDisplay/Code.stories.tsx @@ -0,0 +1,51 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Code, { Props } from '../../../components/utils/Code'; + +export default { + title: 'Next Right Now/Data display/Code', + component: Code, + argTypes: {}, +} as Meta; + +const defaultText = ` + import { css } from '@emotion/react'; + import React from 'react'; + import AnimatedLoader from '../svg/AnimatedLoader'; + + export type Props = {} + + const Loader: React.FunctionComponent = (props): JSX.Element => { + return ( +
+ +
+ ); + }; + + export default Loader; +`; + +const Template: Story = (props) => { + return ( + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: defaultText, +}; diff --git a/src/stories/nrn/dataDisplay/DocumentButton.stories.tsx b/src/stories/nrn/dataDisplay/DocumentButton.stories.tsx new file mode 100644 index 000000000..0e188aacb --- /dev/null +++ b/src/stories/nrn/dataDisplay/DocumentButton.stories.tsx @@ -0,0 +1,35 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import DocumentButton, { Props } from '../../../components/utils/DocumentButton'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string; +}; + +export default { + title: 'Next Right Now/Data display/DocumentButton', + component: DocumentButton, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { text } = props; + + return ( + // @ts-ignore + + {text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: 'My awesome PDF file', +}; diff --git a/src/stories/nrn/dataDisplay/ExternalLink.stories.tsx b/src/stories/nrn/dataDisplay/ExternalLink.stories.tsx new file mode 100644 index 000000000..429d01ae4 --- /dev/null +++ b/src/stories/nrn/dataDisplay/ExternalLink.stories.tsx @@ -0,0 +1,42 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import ExternalLink, { Props } from '../../../components/utils/ExternalLink'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string; +}; + +export default { + title: 'Next Right Now/Data display/ExternalLink', + component: ExternalLink, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { text } = props; + + return ( + // @ts-ignore + console.info('Click')} + > + {text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: 'Open (another tab)', + href: '/', + nofollow: true, + noopener: true, + noreferrer: false, + prefix: ' ', + suffix: ' ', +}; diff --git a/src/stories/nrn/dataDisplay/LinkButton.stories.tsx b/src/stories/nrn/dataDisplay/LinkButton.stories.tsx new file mode 100644 index 000000000..c0a7f9ec0 --- /dev/null +++ b/src/stories/nrn/dataDisplay/LinkButton.stories.tsx @@ -0,0 +1,58 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import ExternalLink from '../../../components/utils/ExternalLink'; +import LinkButton, { Props } from '../../../components/utils/LinkButton'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string; + useLink?: boolean; +}; + +export default { + title: 'Next Right Now/Data display/LinkButton', + component: LinkButton, + subcomponents: { ExternalLink }, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { + text, + useLink, + } = props; + + if (useLink) { + return ( + + + {text || 'Default text'} + + + ); + } + + return ( + + {text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: `Do nothing`, +}; + +export const DynamicExampleWithLink: Story = Template.bind({}); +DynamicExampleWithLink.args = { + text: 'Open external link', + useLink: true, +}; diff --git a/src/stories/nrn/dataDisplay/Markdown.stories.tsx b/src/stories/nrn/dataDisplay/Markdown.stories.tsx new file mode 100644 index 000000000..9be71a102 --- /dev/null +++ b/src/stories/nrn/dataDisplay/Markdown.stories.tsx @@ -0,0 +1,46 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Markdown, { Props } from '../../../components/utils/Markdown'; + +export default { + title: 'Next Right Now/Data display/Markdown', + component: Markdown, + argTypes: {}, +} as Meta; + +const Template: Story = (props) => { + const { text } = props; + + return ( + + {text} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: ` +# This is a Markdown component + +You can use it to build content using Markdown. + +**Awesome markdown** we love it! + +> It's easy to write content using _markdown_. + +--- + +# Using markdownOptions.disableParsingRawHTML = true + +You can even use *whitelisted* React components here! + +A themed button +`, + markdownOptions: { + disableParsingRawHTML: false, + }, +}; diff --git a/src/stories/nrn/dataDisplay/SpoilerLink.stories.tsx b/src/stories/nrn/dataDisplay/SpoilerLink.stories.tsx new file mode 100644 index 000000000..51d69069b --- /dev/null +++ b/src/stories/nrn/dataDisplay/SpoilerLink.stories.tsx @@ -0,0 +1,45 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Btn from '../../../components/utils/Btn'; +import SpoilerLink, { Props } from '../../../components/utils/SpoilerLink'; + +export default { + title: 'Next Right Now/Data display/SpoilerLink', + component: SpoilerLink, + argTypes: {}, +} as Meta; + +const Template: Story = (props) => { + return ( + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + previewElement: ( + Spoil me! + ), + spoilerElement: ( + Arya Stark's phone number is 000000000 + ), + className: '', + href: 'tel:000000000', +}; + +export const DynamicExampleWithBtn: Story = Template.bind({}); +DynamicExampleWithBtn.args = { + previewElement: ( + Spoil me! + ), + spoilerElement: ( + Arya Stark's phone number is 000000000 + ), + className: '', + href: 'tel:000000000', +}; diff --git a/src/stories/nrn/dataDisplay/Stamp.stories.tsx b/src/stories/nrn/dataDisplay/Stamp.stories.tsx new file mode 100644 index 000000000..7778194e8 --- /dev/null +++ b/src/stories/nrn/dataDisplay/Stamp.stories.tsx @@ -0,0 +1,43 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import EllipsisText from '../../../components/utils/EllipsisText'; +import Stamp, { Props } from '../../../components/utils/Stamp'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string | React.ReactElement; +}; + +export default { + title: 'Next Right Now/Data display/Stamp', + component: Stamp, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { text, children } = props; + + return ( + + {children || text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: 'Hello', +}; + + +export const DynamicExampleWithEllipsis: Story = Template.bind({}); +DynamicExampleWithEllipsis.args = { + children: ( + Some very long text, too long to display entirely + ), +}; diff --git a/src/stories/nrn/dataDisplay/Text.stories.tsx b/src/stories/nrn/dataDisplay/Text.stories.tsx new file mode 100644 index 000000000..274f98b74 --- /dev/null +++ b/src/stories/nrn/dataDisplay/Text.stories.tsx @@ -0,0 +1,42 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Text, { Props } from '../../../components/utils/Text'; +import withPropMock from '../../shared/hocs/withPropMock'; + +type PropsWithChildrenMock = Props & { + text?: string; +}; + +export default { + title: 'Next Right Now/Data display/Text', + component: Text, + argTypes: withPropMock({}), +} as Meta; + +const Template: Story = (props) => { + const { text } = props; + + return ( + + {text || 'Default text'} + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + text: ` +

Some text

+ +That can be displayed on multiple lines, +quite easily, +without having to use HTML "<br />" to break lines. + +Useful sometimes, for documentation purpose. +`, +}; diff --git a/src/stories/nrn/dataDisplay/ToggleButton.stories.tsx b/src/stories/nrn/dataDisplay/ToggleButton.stories.tsx new file mode 100644 index 000000000..cc0e66a8e --- /dev/null +++ b/src/stories/nrn/dataDisplay/ToggleButton.stories.tsx @@ -0,0 +1,68 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React, { useState } from 'react'; +import ToggleButton, { Props } from '../../../components/utils/ToggleButton'; + +export default { + title: 'Next Right Now/Data display/ToggleButton', + component: ToggleButton, + argTypes: {}, +} as Meta; + +const Template: Story = (props) => { + const { + contentOn, + contentOff, + } = props; + const [ + isChecked, + setIsChecked, + ] = useState(false); + console.log('isChecked', isChecked); + + return ( + // @ts-ignore + setIsChecked(!isChecked)} + {...props} + /> + ); +}; + +export const FlatExample: Story = Template.bind({}); +FlatExample.args = { + id: 'FlatExample', + mode: 'flat', +}; + +export const FlipExample: Story = Template.bind({}); +FlipExample.args = { + id: 'FlipExample', + mode: 'flip', + flipModeOptions: { + useBackgroundColor: true, + }, +}; + +export const IosExample: Story = Template.bind({}); +IosExample.args = { + id: 'IosExample', + mode: 'ios', +}; + +export const LightExample: Story = Template.bind({}); +LightExample.args = { + id: 'LightExample', + mode: 'light', +}; + +export const SkewedExample: Story = Template.bind({}); +SkewedExample.args = { + id: 'SkewedExample', + mode: 'skewed', +}; diff --git a/src/stories/nrn/dataDisplay/ToggleLanguagesButton.stories.tsx b/src/stories/nrn/dataDisplay/ToggleLanguagesButton.stories.tsx new file mode 100644 index 000000000..fa2e2f76c --- /dev/null +++ b/src/stories/nrn/dataDisplay/ToggleLanguagesButton.stories.tsx @@ -0,0 +1,34 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React, { useState } from 'react'; +import ToggleLanguagesButton, { Props } from '../../../components/utils/ToggleLanguagesButton'; + +export default { + title: 'Next Right Now/Data display/ToggleLanguagesButton', + component: ToggleLanguagesButton, + argTypes: {}, +} as Meta; + +const Template: Story = (props) => { + const [ + isChecked, + setIsChecked, + ] = useState(false); + console.log('isChecked', isChecked); + + return ( + // @ts-ignore + setIsChecked(!isChecked)} + {...props} + /> + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + id: 'toggle-languages-button', +}; diff --git a/src/stories/nrn/i18n/I18nBtnChangeLocale.stories.tsx b/src/stories/nrn/i18n/I18nBtnChangeLocale.stories.tsx new file mode 100644 index 000000000..615af9040 --- /dev/null +++ b/src/stories/nrn/i18n/I18nBtnChangeLocale.stories.tsx @@ -0,0 +1,25 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import I18nBtnChangeLocale, { Props } from '../../../components/i18n/I18nBtnChangeLocale'; + +export default { + title: 'Next Right Now/I18n/I18nBtnChangeLocale', + component: I18nBtnChangeLocale, + argTypes: {}, +} as Meta; + +const Template: Story = (props) => { + return ( + + ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + id: 'default-example', +}; diff --git a/src/stories/nrn/i18n/I18nLink.module.css b/src/stories/nrn/i18n/I18nLink.module.css new file mode 100644 index 000000000..174aa2559 --- /dev/null +++ b/src/stories/nrn/i18n/I18nLink.module.css @@ -0,0 +1,6 @@ +/* This is a basic example to check Storybook supports using CSS Modules */ +.red { + margin: 10px; + padding: 10px; + background-color: red; +} diff --git a/src/stories/nrn/i18n/I18nLink.stories.tsx b/src/stories/nrn/i18n/I18nLink.stories.tsx new file mode 100644 index 000000000..8363e3a02 --- /dev/null +++ b/src/stories/nrn/i18n/I18nLink.stories.tsx @@ -0,0 +1,74 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import I18nLink, { Props } from '../../../components/i18n/I18nLink'; +import withPropMock from '../../shared/hocs/withPropMock'; +import styles from './I18nLink.module.css'; + +type PropsWithChildrenMock = { + text?: string; +} & Props; + +// @ts-ignore +export default { + title: 'Next Right Now/I18n/I18nLink', + component: I18nLink, + argTypes: withPropMock({ + wrapChildrenAsLink: { + control: { + disable: true, // Disable field because it crashes the UI when being used (expected behavior but bad UX) + }, + }, + }), +} as Meta; + +export const DynamicLink: Story = (props) => { + const { + text, + ...rest + } = props; + + return ( + + {text || 'Default text'} + + ); +}; +DynamicLink.args = { + text: 'Link', + href: '/some/nested/page/[id]', + locale: 'en', + className: 'my-class', + params: { + id: 5, + }, + query: { + userId: 1, + }, +}; + +export const LinkWithCSSModule: Story = () => { + return ( + + Homepage (red) + + ); +}; + +export const LinkWithoutWrapper: Story = () => { + return ( + + Homepage + + ); +}; diff --git a/src/stories/nrn/layout/Cards.stories.tsx b/src/stories/nrn/layout/Cards.stories.tsx new file mode 100644 index 000000000..00bd6fef5 --- /dev/null +++ b/src/stories/nrn/layout/Cards.stories.tsx @@ -0,0 +1,321 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import { + Alert, + Card, + CardBody, + CardSubtitle, + CardText, + CardTitle, +} from 'reactstrap'; +import I18nLink from '../../../components/i18n/I18nLink'; +import Btn from '../../../components/utils/Btn'; +import Cards, { Props } from '../../../components/utils/Cards'; +import ExternalLink from '../../../components/utils/ExternalLink'; + +export default { + title: 'Next Right Now/Layout/Cards', + component: Cards, + argTypes: {}, +} as Meta; + +const Template: Story = (props) => { + return ( + + + +

Hosting on Vercel vendor

+ “Deploy your app online in a breeze” + +
+ + Learn more about the Vercel cloud platform + + + Learn how to configure and use Vercel + + + Learn why we chose Vercel + +
+
+
+
+ + + +

Stages & secrets

+ “How to deal with secrets, using Vercel vendor” + +
+ + Learn more about the "env and stages" concept + + + Learn how to configure Vercel secrets, using the CLI + + + Learn more about their usage and differences + +
+
+
+
+ + + +

SaaS B2B MST

+ “Multi Single Tenancy for SaaS B2B businesses who need it” + + + MST is similar to the monorepo design, where the same source code can be used to deploy multiple instances. + +
+ + Learn more about the "tenancy" concept and what MST means + +
+
+
+
+ + + +

CI/CD

+ “Continuous Integrations and Continuous Deployments made free and easy, using Github Actions” + +
+ + Learn more about the "CI/CD" concept + + + Learn how to setup CI/CD + + + See how to bypass automated CI/CD and deploy manually + +
+
+
+
+ + + +

Static i18n

+ “Content internationalisation using i18next and Locize vendor” + +
+ + Learn more about the "i18n" concept + + + Learn how to use the "Locize" vendor + + + See usage examples + +
+
+
+
+ + + +

Monitoring

+ “Realtime app monitoring using Sentry vendor” + +
+ + Learn more about the "Monitoring" concept + + + Learn how to use the "Sentry" vendor + + + See usage examples + +
+
+
+
+ + + +

API

+ “API fetching using Amplitude vendor (REST))” + +
+ + See usage examples + +
+
+
+
+ + + +

CSS-in-JS

+ “Styling components with Emotion” + +
+ + Learn how to use the "Emotion" library + + + See usage examples + +
+
+
+
+ + + +

Cookies consent

+ “Cookies consent using CookieConsent OSS library” + +
+ + Learn more about the "Cookie consent" library + + + Learn more about user consent and its impact on analytics + +
+
+
+
+ + + +

Analytics

+ “Analytics using Amplitude vendor” + +
+ + Learn more about the "Analytics" concept + + + Learn how to use the "Amplitude" vendor + + + See usage examples + +
+
+
+
+ + + +

Testing

+ “Unit tests using Jest and E2E tests using Cypress” + +
+ + Learn more about the "Testing" concept + + + Learn how to use the "Cypress" library (E2E) + +
+
+
+
+ + + +

Icons

+ “Icons library using Font-Awesome” + +
+ + See all available FA icons + + + See usage examples + +
+
+
+
+ + + +

CSS Animations

+ “Animations using Animate.css” + +
+ + See all available animations + + + See usage examples + +
+
+
+
+ + + +

UI components library

+ “React components using Reactstrap and Bootstrap” + +
+ + See all available Reactstrap components + + + See components examples + +
+
+
+
+ + + +

Docs site

+ “Dedicated GitHub pages website, using Jekyll” + +
+ + Learn more about "GitHub pages" + + + Learn more about "just-the-docs" built-in template + + + Learn how to use it + +
+
+
+
+ + + +

Markdown as JSX components at runtime

+ “Dynamically transform Markdown into JSX components at runtime” + +
+ + See usage examples + +
+
+
+
+
+ ); +}; + +export const DynamicExample: Story = Template.bind({}); +DynamicExample.args = { + maxCards: 3, +}; diff --git a/src/stories/nrn/layout/DefaultErrorLayout.stories.tsx b/src/stories/nrn/layout/DefaultErrorLayout.stories.tsx new file mode 100644 index 000000000..715e9d93e --- /dev/null +++ b/src/stories/nrn/layout/DefaultErrorLayout.stories.tsx @@ -0,0 +1,36 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import DefaultErrorLayout, { Props } from '../../../components/errors/DefaultErrorLayout'; + +export default { + title: 'Next Right Now/Layout/DefaultErrorLayout', + component: DefaultErrorLayout, + argTypes: {}, +} as Meta; + +export const ErrorInDefaultLayout: Story = () => { + return ( + + ); +}; + +export const ErrorInDefaultLayoutWithContext: Story = () => { + return ( + + ); +}; diff --git a/src/stories/nrn/layout/Footer.stories.tsx b/src/stories/nrn/layout/Footer.stories.tsx new file mode 100644 index 000000000..c8abb8478 --- /dev/null +++ b/src/stories/nrn/layout/Footer.stories.tsx @@ -0,0 +1,18 @@ +import { + Meta, + Story, +} from '@storybook/react/types-6-0'; +import React from 'react'; +import Footer, { Props } from '../../../components/pageLayouts/Footer'; + +export default { + title: 'Next Right Now/Layout/Footer', + component: Footer, + argTypes: {}, +} as Meta; + +export const DefaultExample: Story = () => { + return ( +