diff --git a/.eslintignore b/.eslintignore index b955f23b7..e251729de 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ node_modules .next +cypress src/**/*.test* src/gql/** src/propTypes/** diff --git a/cypress/README.md b/cypress/README.md index 7ebb08a81..8b402e33b 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -10,6 +10,44 @@ Read more about [the folder structure](https://docs.cypress.io/guides/core-conce - `screenshot`/`videos`: Will contain assets when tests fail, useful during development. When tests are executed by Github Actions, you'll find those assets under the "Artifacts" section. (e.g: https://github.com/UnlyEd/next-right-now/runs/862302266) - `fixtures`: Fixtures are used as external pieces of static data that can be used by your tests. (We've kept the fixtures installed during the Cypress initial install) +## Cypress config files + +The files `cypress/config-*` are used for different purposes. + +- `config-customer-ci-cd.json`: This file is a mock config file used by CI/CD GitHub Actions by the workflows `deploy-vercel-production.yml` and `deploy-vercel-staging.yml`. + The `baseUrl` is a fake value (required by Cypress, but not used) which is replaced at runtime by the real `baseUrl` which is a dynamic Vercel deployment url. +- `config-development.json`: This file is only used when running `yarn e2e:run` and `yarn e2e:open` locally. + It uses `baseUrl=http://localhost:8888` which is where our local server is running. It's only meant for local testing +- `config-$CUSTOMER_REF.json`: This file is only used when running `yarn deploy:$CUSTOMER_REF` locally. _It is not used by CI/CD workflows._ + ## Tests ordering [Sanity Checks](./integration/app/_sanity/README.md) are executed first. Then, tests are executed by their folder/file alphabetical order by default. + +## Resources about how to write tests better + +- [[MUST WATCH!] Best Practices by the Author (2018) - 27mn](https://docs.cypress.io/examples/examples/tutorials.html#Best-Practices) +- [Organize tests by type of devices (mobile/desktop)](https://docs.cypress.io/api/commands/viewport.html#Width-Height) +- [Run tests on multiple subdomains](https://docs.cypress.io/faq/questions/using-cypress-faq.html#Can-I-run-the-same-tests-on-multiple-subdomains) +- [Detect if Cypress is running](https://docs.cypress.io/faq/questions/using-cypress-faq.html#Is-there-any-way-to-detect-if-my-app-is-running-under-Cypress) +- [Can my tests interact with Redux / Vuex data store? (AKA "Dynamic testing")](https://docs.cypress.io/faq/questions/using-cypress-faq.html#Can-my-tests-interact-with-Redux-Vuex-data-store) +- [Check a custom property from the `window` object](https://docs.cypress.io/api/commands/window.html#Check-a-custom-property) +- [Dynamic tests](https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/fundamentals__dynamic-tests) +- [Filters and data-driven tests](https://docs.cypress.io/examples/examples/tutorials.html#7-Filters-and-data-driven-tests) +- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app/blob/develop/cypress/tests/ui/transaction-feeds.spec.ts) + +## Officiel Cypress recommandations + +> We see organizations starting with Cypress by placing end-to-end tests in a separate repo. +> This is a great practice that allows someone on the team to prototype a few tests and evaluate Cypress within minutes. +> As the time passes and the number of tests grows, we strongly suggest moving end-to-end tests to live right alongside your front end code. +> +> This brings many benefits: +> - engages developers in writing end-to-end tests sooner +> - keeps tests and the features they test in sync +> - tests can be run every time the code changes +> - allows code sharing between the application code and the tests (like selectors) + +_[Source](https://docs.cypress.io/faq/questions/using-cypress-faq.html#What-are-your-best-practices-for-organizing-tests)_ + +[Cypress releases "Real World App" (RWA) - Blog post](https://www.cypress.io/blog/2020/06/11/introducing-the-cypress-real-world-app/) diff --git a/cypress/config-customer-ci-cd.json b/cypress/config-customer-ci-cd.json index d54634f25..0e94607db 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 and not meant to be used locally", + "//": "This file is used by CI/CD GitHub Actions (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-development.json b/cypress/config-development.json index ede0bff64..ceae0f975 100644 --- a/cypress/config-development.json +++ b/cypress/config-development.json @@ -4,5 +4,6 @@ "screenshotsFolder": "cypress/screenshots/development", "videosFolder": "cypress/videos/development", "env": {}, - "ignoreTestFiles": "*.md" + "ignoreTestFiles": "*.md", + "experimentalComponentTesting": true } diff --git a/cypress/global.d.ts b/cypress/global.d.ts new file mode 100644 index 000000000..6d4cbd5a6 --- /dev/null +++ b/cypress/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/cypress/integration/app/_sanity/domain.js b/cypress/integration/app/_sanity/1-domain.ts similarity index 56% rename from cypress/integration/app/_sanity/domain.js rename to cypress/integration/app/_sanity/1-domain.ts index 1245ecf39..f2933e704 100644 --- a/cypress/integration/app/_sanity/domain.js +++ b/cypress/integration/app/_sanity/1-domain.ts @@ -1,9 +1,9 @@ const baseUrl = Cypress.config().baseUrl; describe('Sanity checks > Domain', { - retries: { - runMode: 2, // Allows 2 retries (for a total of 3 attempts) to reduce the probability of failing the whole tests suite because Vercel hasn't finished to deploy yet (which makes Cypress fail by trying to test the Vercel "waiting page", instead of our app) - } + // retries: { + // runMode: 2, // Allows 2 retries (for a total of 3 attempts) to reduce the probability of failing the whole tests suite because Vercel hasn't finished to deploy yet (which makes Cypress fail by trying to test the Vercel "waiting page", instead of our app) + // } }, () => { /* * Visits the home page before any test @@ -12,7 +12,7 @@ describe('Sanity checks > Domain', { cy.visit('/en'); }); - it('should be running on the right domain', () => { + it(`should be running on the domain "${baseUrl}"`, () => { cy.url().then((url) => { cy.log(`Expected to be running on:`); cy.log(baseUrl); @@ -22,3 +22,5 @@ describe('Sanity checks > Domain', { }); }); }); + +export {}; diff --git a/cypress/integration/app/_sanity/2-customer.ts b/cypress/integration/app/_sanity/2-customer.ts new file mode 100644 index 000000000..3f13f9ab2 --- /dev/null +++ b/cypress/integration/app/_sanity/2-customer.ts @@ -0,0 +1,31 @@ +import { Customer } from '../../../../src/types/data/Customer'; +import { CYPRESS_WINDOW_NS } from '../../../../src/utils/testing/cypress'; + +describe('Sanity checks > Browser data', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it(`should have "window.${CYPRESS_WINDOW_NS}.dataset" defined`, () => { + cy.get('@dataset').then((dataset) => { + assert.isDefined(dataset); + expect(Object.keys(dataset).length).to.be.greaterThan(0); + }); + }); + + it(`should have "window.${CYPRESS_WINDOW_NS}.customer" defined`, () => { + cy.get('@customer').then((customer: Customer) => { + assert.isDefined(customer.label); + }); + }); +}); diff --git a/cypress/integration/app/common/footer.js b/cypress/integration/app/common/footer.js deleted file mode 100644 index 86711611d..000000000 --- a/cypress/integration/app/common/footer.js +++ /dev/null @@ -1,23 +0,0 @@ -const baseUrl = Cypress.config().baseUrl; - -describe('Common > Footer section', () => { - /* - * Visits the home page before any test - */ - before(() => { - cy.visit('/en'); - }); - - it('should have the Unly logo in the footer', () => { - cy.get('#footer-logo-unly-brand').should('have.length', 1); - }); - - it('should have the customer logo in the footer', () => { - cy.get('#footer-logo-organisation-brand').should('have.length', 1); - }); - - it('should have a button to change the language which changes the language upon click', () => { - cy.get('.btn-change-locale').should('have.length', 1).click(); - cy.url().should('eq', `${baseUrl}/fr`); - }); -}); diff --git a/cypress/integration/app/common/footer.ts b/cypress/integration/app/common/footer.ts new file mode 100644 index 000000000..3e55dcb52 --- /dev/null +++ b/cypress/integration/app/common/footer.ts @@ -0,0 +1,48 @@ +import { Customer } from '../../../../src/types/data/Customer'; + +const baseUrl = Cypress.config().baseUrl; + +describe('Common > Footer section', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it('should have the Unly logo in the footer', () => { + cy.get('#footer-logo-unly-brand').should('have.length', 1); + }); + + it('should have the customer logo in the footer', () => { + cy.get('#footer-logo').should('have.length', 1); + }); + + it('should display the i18n button to change language', () => { + cy.get('@customer').then((customer: Customer) => { + const availableLanguagesCount = 2; + cy.log(`Available language(s): ${availableLanguagesCount}`); + + if (availableLanguagesCount > 1) { + it('should have a button to change the language which changes the language upon click', () => { + cy.get('#footer-btn-change-locale').should('have.length', 1).click({ force: true }); + cy.url().should('eq', `${baseUrl}/fr`); + }); + } else { + it('should not have a button to change the language', () => { + cy.get('#footer-btn-change-locale').should('not.have.length', 1); + }); + } + }); + }); + +}); + +export {}; diff --git a/cypress/integration/app/common/nav.js b/cypress/integration/app/common/nav.js deleted file mode 100644 index e3b17d453..000000000 --- a/cypress/integration/app/common/nav.js +++ /dev/null @@ -1,43 +0,0 @@ -const baseUrl = Cypress.config().baseUrl; - -describe('Common > Nav section', () => { - /* - * Visits the home page before any test - */ - beforeEach(() => { - cy.visit('/en'); - }); - - it('should have 5 links in the navigation bar', () => { - cy.get('#nav .navbar-nav > .nav-item').should('have.length', 5); - }); - - it('should have a link in the navbar that redirects to the home page', () => { - cy.get('#nav-link-home') - .should('have.text', 'Home') - .click(); - cy.url({ timeout: 10000 }).should('eq', `${baseUrl}/en`); - }); - - it('should have a link in the navbar that redirects to the built-in feature "Static I18n"', () => { - cy.get('#nav-link-examples') - .should('have.text', 'Examples') - .click(); - cy.get('#nav-link-examples-static-i-18-n') - .should('have.text', 'Static i18n') - .click(); - cy.url({ timeout: 45000 }).should('eq', `${baseUrl}/en/examples/built-in-features/static-i18n`); - cy.get('h1').should('have.length', 1).should('have.text', 'Static i18n examples, using i18next and Locize vendor'); - }); - - it('should have a link in the navbar that redirects to the native feature "SSR"', () => { - cy.get('#nav-link-examples') - .should('have.text', 'Examples') - .click(); - cy.get('#nav-link-examples-ssr-get-server-side-props') - .should('have.text', 'SSR (getServerSideProps)') - .click(); - cy.url({ timeout: 45000 }).should('eq', `${baseUrl}/en/examples/native-features/example-with-ssr`); - cy.get('h1').should('have.length', 1).should('have.text', 'Example, using SSR'); - }); -}); diff --git a/cypress/integration/app/common/nav.ts b/cypress/integration/app/common/nav.ts new file mode 100644 index 000000000..878d2e965 --- /dev/null +++ b/cypress/integration/app/common/nav.ts @@ -0,0 +1,35 @@ +import { Customer } from '../../../../src/types/data/Customer'; + +const baseUrl = Cypress.config().baseUrl; + +describe('Common > Nav section', () => { + /** + * Visits the home page before any test. + */ + before(() => { + cy.visit('/en'); + }); + + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + + it('should have 3 links in the navigation bar', () => { + cy.get('#nav .navbar-nav > .nav-item').should('have.length', 5); + }); + + it('should have a link in the navbar that redirects to the home page', () => { + cy.get('@customer').then((customer: Customer) => { + const isPageInEnglish = true; + cy.get('#nav-link-home') + .should('have.text', isPageInEnglish ? 'Home' : 'Accueil') + .click(); + cy.url({ timeout: 10000 }).should('eq', `${baseUrl}/${isPageInEnglish ? 'en' : 'fr'}`); + }); + }); +}); + +export {}; diff --git a/cypress/integration/app/pages/index.js b/cypress/integration/app/pages/index.ts similarity index 54% rename from cypress/integration/app/pages/index.js rename to cypress/integration/app/pages/index.ts index 6ece23991..df098f959 100644 --- a/cypress/integration/app/pages/index.js +++ b/cypress/integration/app/pages/index.ts @@ -1,14 +1,23 @@ const baseUrl = Cypress.config().baseUrl; describe('Index page', () => { - /* - * Visits the home page before any test - */ + /** + * Visits the home page before any test. + */ before(() => { cy.visit('/en'); }); + /** + * Prepare aliases before each test. (they're destroyed at the end of each test) + */ + beforeEach(() => { + cy.prepareDOMAliases(); + }); + it('should display a main title', () => { cy.get('h1').should('have.length', 1).should('have.text', 'Next Right Now Demo'); }); }); + +export {}; diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index fd170fba6..7d9ae3d2c 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -11,7 +11,10 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) +/// + module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config + return config; } diff --git a/cypress/support/commands.d.ts b/cypress/support/commands.d.ts new file mode 100644 index 000000000..7dbe007be --- /dev/null +++ b/cypress/support/commands.d.ts @@ -0,0 +1,5 @@ +declare namespace Cypress { + interface cy extends Chainable { + prepareDOMAliases: () => Chainable; + } +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ca4d256f3..6d46eb582 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -7,19 +7,27 @@ // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +import { CYPRESS_WINDOW_NS } from '../../src/utils/testing/cypress'; + +/** + * Prepare DOM aliases by fetching the customer data from the browser window and aliasing them for later use. + * + * @example cy.prepareDOMAliases(); + */ +Cypress.Commands.add('prepareDOMAliases', () => { + return cy.window().then((window) => { + cy.get('.page-wrapper').then(() => { // Wait for the DOM element to be created by Next.js before trying to read any dynamic data from the "window" object + cy.log(`window[${CYPRESS_WINDOW_NS}]`, window[CYPRESS_WINDOW_NS]); + + const { + customer, + dataset, + } = window[CYPRESS_WINDOW_NS]; + + // Use aliases to make our variables reusable across tests - See https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Sharing-Context + cy.wrap(customer).as('customer'); + cy.wrap(dataset).as('dataset'); + }); + }); +}); diff --git a/cypress/support/index.js b/cypress/support/index.js index 1f733754f..09fc832bc 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -13,7 +13,8 @@ // https://on.cypress.io/configuration // *********************************************************** -// Import commands.js using ES2015 syntax: +// See https://dev.to/cuichenli/how-do-i-setup-my-nextjs-development-environment-2kao +import 'cypress-react-unit-test/support'; import './commands'; // See https://docs.cypress.io/api/events/catalog-of-events.html#Uncaught-Exceptions diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 000000000..e5079d008 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "./**/*.ts*" + ], + "exclude": [], + "compilerOptions": { + "baseUrl": ".", + "jsx": "react", + "types": [ + "cypress" + ], + "sourceMap": false, + "isolatedModules": true + } +} diff --git a/jest.d.ts b/jest.d.ts index de6493e54..b086bf019 100644 --- a/jest.d.ts +++ b/jest.d.ts @@ -9,6 +9,7 @@ declare global { namespace NodeJS { interface Global { muteConsole: () => any; + muteConsoleButLog: () => any; unmuteConsole: () => any; } } diff --git a/jest.setup.js b/jest.setup.js index 3a086ef71..3c0daf650 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -28,5 +28,19 @@ global.muteConsole = () => { }; }; +// Force mute console by returning a mock object that mocks the props we use, except for "log" +global.muteConsoleButLog = () => { + return { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + log: _console.log, + warn: jest.fn(), + }; +}; + // Restore previously made "console" object global.unmuteConsole = () => _console; + +// Mock __non_webpack_require__ to use the standard node.js "require" +global['__non_webpack_require__'] = require; diff --git a/next.config.js b/next.config.js index 7b406793a..4577f9748 100644 --- a/next.config.js +++ b/next.config.js @@ -103,7 +103,23 @@ module.exports = withBundleAnalyzer(withSourceMaps({ async headers() { const headers = []; - console.info('Using headers:', headers); + // XXX Forbid usage in iframes from external 3rd parties, for non-production site + // This is meant to avoid customers using the preview in their production website, which would incur uncontrolled costs on our end + // Also, our preview env cannot scale considering each request send many airtable API calls and those are rate limited and out of our control + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { + headers.push({ + source: '/(.*?)', // Match all paths, including "/" - See https://github.com/vercel/next.js/discussions/17991#discussioncomment-112028 + // source: '/:path*', // Match all paths, excluding "/" + headers: [ + { + key: 'Content-Security-Policy', + value: 'frame-ancestors *.stacker.app', + }, + ], + }); + } + + console.info('Using headers:', JSON.stringify(headers, null, 2)); return headers; }, diff --git a/package.json b/package.json index ec0d0e552..39845ef9d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "start:tunnel": "ngrok http 8888", "build": "yarn test:once && next build", "build:profiler": "next build --profile", - "analyse:bundle": "ANALYZE_BUNDLE=true yarn start", + "analyse:bundle": "yarn analyse:bundle:production", + "analyse:bundle:development": "ANALYZE_BUNDLE=true yarn start", + "analyse:bundle:production": "ANALYZE_BUNDLE=true next build", "analyse:unused": "next-unused", "svg": "npx svgr -d src/svg src/svg --ext tsx --template src/utils/svg/svgTemplate.ts", "deploy": "yarn deploy:customer1", @@ -20,7 +22,7 @@ "deploy:customer1:production": "yarn deploy:customer1:production:simple && yarn e2e:customer1:production", "deploy:customer1:production:simple": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer1.production.json --prod", "deploy:customer2:all": "yarn deploy:customer2 && yarn deploy:customer2:production", - "deploy:customer2": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer2.staging.json --debug", + "deploy:customer2": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer2.staging.json --debug", "deploy:customer2:production": "yarn deploy:customer2:production:simple && yarn e2e:customer2:production", "deploy:customer2:production:simple": "yarn vercel:cleanup && yarn vercel:deploy --local-config=vercel.customer2.production.json --prod", "deploy:fake": "git commit --allow-empty -m \"Fake empty commit (force CI trigger)\"", @@ -46,7 +48,7 @@ "lint:once": "eslint src/ --ext .ts --ext .tsx", "lint:fix": "eslint src/ --ext .ts --ext .tsx --fix", "lint:fix:preview": "eslint src/ --ext .ts --ext .tsx --fix-dry-run", - "test": "NODE_ENV=test jest --watchAll", + "test": "NODE_ENV=test jest --watch", "test:group:api": "NODE_ENV=test jest --group=api --watchAll", "test:group:components": "NODE_ENV=test jest --group=components --watchAll", "test:group:integration": "NODE_ENV=test jest --group=integration --watchAll", @@ -96,6 +98,7 @@ "@unly/utils-simple-logger": "1.4.0", "amplitude-js": "7.1.1", "animate.css": "4.1.1", + "append-query": "2.1.0", "apollo-boost": "0.4.9", "apollo-cache-inmemory": "1.6.6", "apollo-client": "2.6.10", @@ -119,13 +122,16 @@ "lodash.clonedeep": "4.5.0", "lodash.filter": "4.6.0", "lodash.find": "4.6.0", + "lodash.findindex": "4.6.0", "lodash.get": "4.4.2", + "lodash.groupby": "4.6.0", "lodash.includes": "4.3.0", "lodash.isarray": "4.0.0", "lodash.isempty": "4.4.0", "lodash.isplainobject": "4.0.6", "lodash.kebabcase": "4.1.1", "lodash.map": "4.6.0", + "lodash.reduce": "4.6.0", "lodash.remove": "4.7.0", "lodash.size": "4.2.0", "lodash.some": "4.6.0", @@ -145,6 +151,8 @@ "react-style-proptype": "3.2.2", "reactstrap": "8.5.1", "recompose": "0.30.0", + "tinycolor2": "1.4.2", + "type-fest": "0.16.0", "uuid": "8.3.0", "webfontloader": "1.6.28", "winston": "3.2.1" @@ -160,13 +168,16 @@ "@types/js-cookie": "2.2.6", "@types/lodash.clonedeep": "4.5.6", "@types/lodash.find": "4.6.6", + "@types/lodash.findindex": "4.6.6", "@types/lodash.get": "4.4.6", + "@types/lodash.groupby": "4.6.6", "@types/lodash.includes": "4.3.6", "@types/lodash.isarray": "4.0.6", "@types/lodash.isempty": "4.4.6", "@types/lodash.isplainobject": "4.0.6", "@types/lodash.kebabcase": "4.1.6", "@types/lodash.map": "4.6.13", + "@types/lodash.reduce": "4.6.6", "@types/lodash.remove": "4.7.6", "@types/lodash.size": "4.2.6", "@types/lodash.some": "4.6.6", @@ -187,7 +198,8 @@ "concurrently": "5.3.0", "cross-env": "7.0.2", "current-git-branch": "1.1.0", - "cypress": "5.1.0", + "cypress": "6.0.1", + "cypress-react-unit-test": "4.17.1", "del-cli": "3.0.1", "dotenv": "8.2.0", "eslint": "7.8.1", diff --git a/public/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg b/public/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg new file mode 100644 index 000000000..f416f76ee --- /dev/null +++ b/public/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg @@ -0,0 +1 @@ + diff --git a/public/static/images/LOGO_Powered_by_UNLY_monochrome_WHITE.svg b/public/static/images/LOGO_Powered_by_UNLY_monochrome_WHITE.svg deleted file mode 100644 index 8bc2f018e..000000000 --- a/public/static/images/LOGO_Powered_by_UNLY_monochrome_WHITE.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - diff --git a/schema.graphql b/schema.graphql index f9d8d917f..044c4ab6c 100644 --- a/schema.graphql +++ b/schema.graphql @@ -150,6 +150,7 @@ type Color { "A B2B customer. All content belongs to a customer." type Customer implements Node { + availableLanguages: AvailableLanguages "The time the document was created" createdAt( "Variation of DateTime field to return, allows value from base document, current localization, or combined by returning the newer value of both" @@ -183,6 +184,8 @@ type Customer implements Node { "Potential locales that should be returned" locales: [Locale!]! = [en, fr] ): [Customer!]! + privacyDescription: RichText + "Products" products( after: String, before: String, @@ -210,7 +213,7 @@ type Customer implements Node { ref: String! "System stage field" stage: Stage! - terms: RichText + termsDescription: RichText theme( """ @@ -1147,6 +1150,7 @@ type RichText { } type Theme implements Node { + backgroundColor: String "The time the document was created" createdAt: DateTime! customer( @@ -1170,6 +1174,8 @@ type Theme implements Node { "Potential stages that should be returned" stages: [Stage!]! = [PUBLISHED, DRAFT] ): [Theme!]! + errorColor: String + fonts: String "List of Theme versions" history( limit: Int! = 10, @@ -1191,11 +1197,20 @@ type Theme implements Node { """ locales: [Locale!] ): Asset! + onBackgroundColor: String + onErrorColor: String + onPrimaryColor: String + onSecondaryColor: String + onSurfaceColor: String primaryColor: String! + primaryColorVariant1: String "The time the document was published. Null on documents in draft stage." publishedAt: DateTime + secondaryColor: String + secondaryColorVariant1: String "System stage field" stage: Stage! + surfaceColor: String "The time the document was updated" updatedAt: DateTime! } @@ -1249,7 +1264,14 @@ enum AssetOrderByInput { width_DESC } +enum AvailableLanguages { + en + fr +} + enum CustomerOrderByInput { + availableLanguages_ASC + availableLanguages_DESC createdAt_ASC createdAt_DESC id_ASC @@ -1332,14 +1354,38 @@ enum SystemDateTimeFieldVariation { } enum ThemeOrderByInput { + backgroundColor_ASC + backgroundColor_DESC createdAt_ASC createdAt_DESC + errorColor_ASC + errorColor_DESC + fonts_ASC + fonts_DESC id_ASC id_DESC + onBackgroundColor_ASC + onBackgroundColor_DESC + onErrorColor_ASC + onErrorColor_DESC + onPrimaryColor_ASC + onPrimaryColor_DESC + onSecondaryColor_ASC + onSecondaryColor_DESC + onSurfaceColor_ASC + onSurfaceColor_DESC + primaryColorVariant1_ASC + primaryColorVariant1_DESC primaryColor_ASC primaryColor_DESC publishedAt_ASC publishedAt_DESC + secondaryColorVariant1_ASC + secondaryColorVariant1_DESC + secondaryColor_ASC + secondaryColor_DESC + surfaceColor_ASC + surfaceColor_DESC updatedAt_ASC updatedAt_DESC } @@ -1962,15 +2008,18 @@ input CustomerConnectInput { } input CustomerCreateInput { + availableLanguages: AvailableLanguages createdAt: DateTime "label input for default locale (en)" label: String "Inline mutations for managing document localizations excluding the default locale" localizations: CustomerCreateLocalizationsInput + "privacyDescription input for default locale (en)" + privacyDescription: RichTextAST products: ProductCreateManyInlineInput ref: String! - "terms input for default locale (en)" - terms: RichTextAST + "termsDescription input for default locale (en)" + termsDescription: RichTextAST theme: ThemeCreateOneInlineInput updatedAt: DateTime } @@ -1978,7 +2027,8 @@ input CustomerCreateInput { input CustomerCreateLocalizationDataInput { createdAt: DateTime label: String - terms: RichTextAST + privacyDescription: RichTextAST + termsDescription: RichTextAST updatedAt: DateTime } @@ -2017,6 +2067,13 @@ input CustomerManyWhereInput { OR: [CustomerWhereInput!] "Contains search across all appropriate fields." _search: String + availableLanguages: AvailableLanguages + "All values that are contained in given list." + availableLanguages_in: [AvailableLanguages!] + "All values that are not equal to given value." + availableLanguages_not: AvailableLanguages + "All values that are not contained in given list." + availableLanguages_not_in: [AvailableLanguages!] createdAt: DateTime "All values greater than the given value." createdAt_gt: DateTime @@ -2107,20 +2164,24 @@ input CustomerManyWhereInput { } input CustomerUpdateInput { + availableLanguages: AvailableLanguages "label input for default locale (en)" label: String "Manage document localizations" localizations: CustomerUpdateLocalizationsInput + "privacyDescription input for default locale (en)" + privacyDescription: RichTextAST products: ProductUpdateManyInlineInput ref: String - "terms input for default locale (en)" - terms: RichTextAST + "termsDescription input for default locale (en)" + termsDescription: RichTextAST theme: ThemeUpdateOneInlineInput } input CustomerUpdateLocalizationDataInput { label: String - terms: RichTextAST + privacyDescription: RichTextAST + termsDescription: RichTextAST } input CustomerUpdateLocalizationInput { @@ -2156,17 +2217,21 @@ input CustomerUpdateManyInlineInput { } input CustomerUpdateManyInput { + availableLanguages: AvailableLanguages "label input for default locale (en)" label: String "Optional updates to localizations" localizations: CustomerUpdateManyLocalizationsInput - "terms input for default locale (en)" - terms: RichTextAST + "privacyDescription input for default locale (en)" + privacyDescription: RichTextAST + "termsDescription input for default locale (en)" + termsDescription: RichTextAST } input CustomerUpdateManyLocalizationDataInput { label: String - terms: RichTextAST + privacyDescription: RichTextAST + termsDescription: RichTextAST } input CustomerUpdateManyLocalizationInput { @@ -2238,6 +2303,13 @@ input CustomerWhereInput { OR: [CustomerWhereInput!] "Contains search across all appropriate fields." _search: String + availableLanguages: AvailableLanguages + "All values that are contained in given list." + availableLanguages_in: [AvailableLanguages!] + "All values that are not equal to given value." + availableLanguages_not: AvailableLanguages + "All values that are not contained in given list." + availableLanguages_not_in: [AvailableLanguages!] createdAt: DateTime "All values greater than the given value." createdAt_gt: DateTime @@ -2842,10 +2914,22 @@ input ThemeConnectInput { } input ThemeCreateInput { + backgroundColor: String createdAt: DateTime customer: CustomerCreateOneInlineInput + errorColor: String + fonts: String logo: AssetCreateOneInlineInput! + onBackgroundColor: String + onErrorColor: String + onPrimaryColor: String + onSecondaryColor: String + onSurfaceColor: String primaryColor: String! + primaryColorVariant1: String + secondaryColor: String + secondaryColorVariant1: String + surfaceColor: String updatedAt: DateTime } @@ -2873,6 +2957,25 @@ input ThemeManyWhereInput { OR: [ThemeWhereInput!] "Contains search across all appropriate fields." _search: String + backgroundColor: String + "All values containing the given string." + backgroundColor_contains: String + "All values ending with the given string." + backgroundColor_ends_with: String + "All values that are contained in given list." + backgroundColor_in: [String!] + "All values that are not equal to given value." + backgroundColor_not: String + "All values not containing the given string." + backgroundColor_not_contains: String + "All values not ending with the given string" + backgroundColor_not_ends_with: String + "All values that are not contained in given list." + backgroundColor_not_in: [String!] + "All values not starting with the given string." + backgroundColor_not_starts_with: String + "All values starting with the given string." + backgroundColor_starts_with: String createdAt: DateTime "All values greater than the given value." createdAt_gt: DateTime @@ -2889,6 +2992,44 @@ input ThemeManyWhereInput { "All values that are not contained in given list." createdAt_not_in: [DateTime!] customer: CustomerWhereInput + errorColor: String + "All values containing the given string." + errorColor_contains: String + "All values ending with the given string." + errorColor_ends_with: String + "All values that are contained in given list." + errorColor_in: [String!] + "All values that are not equal to given value." + errorColor_not: String + "All values not containing the given string." + errorColor_not_contains: String + "All values not ending with the given string" + errorColor_not_ends_with: String + "All values that are not contained in given list." + errorColor_not_in: [String!] + "All values not starting with the given string." + errorColor_not_starts_with: String + "All values starting with the given string." + errorColor_starts_with: String + fonts: String + "All values containing the given string." + fonts_contains: String + "All values ending with the given string." + fonts_ends_with: String + "All values that are contained in given list." + fonts_in: [String!] + "All values that are not equal to given value." + fonts_not: String + "All values not containing the given string." + fonts_not_contains: String + "All values not ending with the given string" + fonts_not_ends_with: String + "All values that are not contained in given list." + fonts_not_in: [String!] + "All values not starting with the given string." + fonts_not_starts_with: String + "All values starting with the given string." + fonts_starts_with: String id: ID "All values containing the given string." id_contains: ID @@ -2909,7 +3050,121 @@ input ThemeManyWhereInput { "All values starting with the given string." id_starts_with: ID logo: AssetWhereInput + onBackgroundColor: String + "All values containing the given string." + onBackgroundColor_contains: String + "All values ending with the given string." + onBackgroundColor_ends_with: String + "All values that are contained in given list." + onBackgroundColor_in: [String!] + "All values that are not equal to given value." + onBackgroundColor_not: String + "All values not containing the given string." + onBackgroundColor_not_contains: String + "All values not ending with the given string" + onBackgroundColor_not_ends_with: String + "All values that are not contained in given list." + onBackgroundColor_not_in: [String!] + "All values not starting with the given string." + onBackgroundColor_not_starts_with: String + "All values starting with the given string." + onBackgroundColor_starts_with: String + onErrorColor: String + "All values containing the given string." + onErrorColor_contains: String + "All values ending with the given string." + onErrorColor_ends_with: String + "All values that are contained in given list." + onErrorColor_in: [String!] + "All values that are not equal to given value." + onErrorColor_not: String + "All values not containing the given string." + onErrorColor_not_contains: String + "All values not ending with the given string" + onErrorColor_not_ends_with: String + "All values that are not contained in given list." + onErrorColor_not_in: [String!] + "All values not starting with the given string." + onErrorColor_not_starts_with: String + "All values starting with the given string." + onErrorColor_starts_with: String + onPrimaryColor: String + "All values containing the given string." + onPrimaryColor_contains: String + "All values ending with the given string." + onPrimaryColor_ends_with: String + "All values that are contained in given list." + onPrimaryColor_in: [String!] + "All values that are not equal to given value." + onPrimaryColor_not: String + "All values not containing the given string." + onPrimaryColor_not_contains: String + "All values not ending with the given string" + onPrimaryColor_not_ends_with: String + "All values that are not contained in given list." + onPrimaryColor_not_in: [String!] + "All values not starting with the given string." + onPrimaryColor_not_starts_with: String + "All values starting with the given string." + onPrimaryColor_starts_with: String + onSecondaryColor: String + "All values containing the given string." + onSecondaryColor_contains: String + "All values ending with the given string." + onSecondaryColor_ends_with: String + "All values that are contained in given list." + onSecondaryColor_in: [String!] + "All values that are not equal to given value." + onSecondaryColor_not: String + "All values not containing the given string." + onSecondaryColor_not_contains: String + "All values not ending with the given string" + onSecondaryColor_not_ends_with: String + "All values that are not contained in given list." + onSecondaryColor_not_in: [String!] + "All values not starting with the given string." + onSecondaryColor_not_starts_with: String + "All values starting with the given string." + onSecondaryColor_starts_with: String + onSurfaceColor: String + "All values containing the given string." + onSurfaceColor_contains: String + "All values ending with the given string." + onSurfaceColor_ends_with: String + "All values that are contained in given list." + onSurfaceColor_in: [String!] + "All values that are not equal to given value." + onSurfaceColor_not: String + "All values not containing the given string." + onSurfaceColor_not_contains: String + "All values not ending with the given string" + onSurfaceColor_not_ends_with: String + "All values that are not contained in given list." + onSurfaceColor_not_in: [String!] + "All values not starting with the given string." + onSurfaceColor_not_starts_with: String + "All values starting with the given string." + onSurfaceColor_starts_with: String primaryColor: String + primaryColorVariant1: String + "All values containing the given string." + primaryColorVariant1_contains: String + "All values ending with the given string." + primaryColorVariant1_ends_with: String + "All values that are contained in given list." + primaryColorVariant1_in: [String!] + "All values that are not equal to given value." + primaryColorVariant1_not: String + "All values not containing the given string." + primaryColorVariant1_not_contains: String + "All values not ending with the given string" + primaryColorVariant1_not_ends_with: String + "All values that are not contained in given list." + primaryColorVariant1_not_in: [String!] + "All values not starting with the given string." + primaryColorVariant1_not_starts_with: String + "All values starting with the given string." + primaryColorVariant1_starts_with: String "All values containing the given string." primaryColor_contains: String "All values ending with the given string." @@ -2943,6 +3198,63 @@ input ThemeManyWhereInput { publishedAt_not: DateTime "All values that are not contained in given list." publishedAt_not_in: [DateTime!] + secondaryColor: String + secondaryColorVariant1: String + "All values containing the given string." + secondaryColorVariant1_contains: String + "All values ending with the given string." + secondaryColorVariant1_ends_with: String + "All values that are contained in given list." + secondaryColorVariant1_in: [String!] + "All values that are not equal to given value." + secondaryColorVariant1_not: String + "All values not containing the given string." + secondaryColorVariant1_not_contains: String + "All values not ending with the given string" + secondaryColorVariant1_not_ends_with: String + "All values that are not contained in given list." + secondaryColorVariant1_not_in: [String!] + "All values not starting with the given string." + secondaryColorVariant1_not_starts_with: String + "All values starting with the given string." + secondaryColorVariant1_starts_with: String + "All values containing the given string." + secondaryColor_contains: String + "All values ending with the given string." + secondaryColor_ends_with: String + "All values that are contained in given list." + secondaryColor_in: [String!] + "All values that are not equal to given value." + secondaryColor_not: String + "All values not containing the given string." + secondaryColor_not_contains: String + "All values not ending with the given string" + secondaryColor_not_ends_with: String + "All values that are not contained in given list." + secondaryColor_not_in: [String!] + "All values not starting with the given string." + secondaryColor_not_starts_with: String + "All values starting with the given string." + secondaryColor_starts_with: String + surfaceColor: String + "All values containing the given string." + surfaceColor_contains: String + "All values ending with the given string." + surfaceColor_ends_with: String + "All values that are contained in given list." + surfaceColor_in: [String!] + "All values that are not equal to given value." + surfaceColor_not: String + "All values not containing the given string." + surfaceColor_not_contains: String + "All values not ending with the given string" + surfaceColor_not_ends_with: String + "All values that are not contained in given list." + surfaceColor_not_in: [String!] + "All values not starting with the given string." + surfaceColor_not_starts_with: String + "All values starting with the given string." + surfaceColor_starts_with: String updatedAt: DateTime "All values greater than the given value." updatedAt_gt: DateTime @@ -2961,9 +3273,21 @@ input ThemeManyWhereInput { } input ThemeUpdateInput { + backgroundColor: String customer: CustomerUpdateOneInlineInput + errorColor: String + fonts: String logo: AssetUpdateOneInlineInput + onBackgroundColor: String + onErrorColor: String + onPrimaryColor: String + onSecondaryColor: String + onSurfaceColor: String primaryColor: String + primaryColorVariant1: String + secondaryColor: String + secondaryColorVariant1: String + surfaceColor: String } input ThemeUpdateManyInlineInput { @@ -2984,7 +3308,19 @@ input ThemeUpdateManyInlineInput { } input ThemeUpdateManyInput { + backgroundColor: String + errorColor: String + fonts: String + onBackgroundColor: String + onErrorColor: String + onPrimaryColor: String + onSecondaryColor: String + onSurfaceColor: String primaryColor: String + primaryColorVariant1: String + secondaryColor: String + secondaryColorVariant1: String + surfaceColor: String } input ThemeUpdateManyWithNestedWhereInput { @@ -3040,6 +3376,25 @@ input ThemeWhereInput { OR: [ThemeWhereInput!] "Contains search across all appropriate fields." _search: String + backgroundColor: String + "All values containing the given string." + backgroundColor_contains: String + "All values ending with the given string." + backgroundColor_ends_with: String + "All values that are contained in given list." + backgroundColor_in: [String!] + "All values that are not equal to given value." + backgroundColor_not: String + "All values not containing the given string." + backgroundColor_not_contains: String + "All values not ending with the given string" + backgroundColor_not_ends_with: String + "All values that are not contained in given list." + backgroundColor_not_in: [String!] + "All values not starting with the given string." + backgroundColor_not_starts_with: String + "All values starting with the given string." + backgroundColor_starts_with: String createdAt: DateTime "All values greater than the given value." createdAt_gt: DateTime @@ -3056,6 +3411,44 @@ input ThemeWhereInput { "All values that are not contained in given list." createdAt_not_in: [DateTime!] customer: CustomerWhereInput + errorColor: String + "All values containing the given string." + errorColor_contains: String + "All values ending with the given string." + errorColor_ends_with: String + "All values that are contained in given list." + errorColor_in: [String!] + "All values that are not equal to given value." + errorColor_not: String + "All values not containing the given string." + errorColor_not_contains: String + "All values not ending with the given string" + errorColor_not_ends_with: String + "All values that are not contained in given list." + errorColor_not_in: [String!] + "All values not starting with the given string." + errorColor_not_starts_with: String + "All values starting with the given string." + errorColor_starts_with: String + fonts: String + "All values containing the given string." + fonts_contains: String + "All values ending with the given string." + fonts_ends_with: String + "All values that are contained in given list." + fonts_in: [String!] + "All values that are not equal to given value." + fonts_not: String + "All values not containing the given string." + fonts_not_contains: String + "All values not ending with the given string" + fonts_not_ends_with: String + "All values that are not contained in given list." + fonts_not_in: [String!] + "All values not starting with the given string." + fonts_not_starts_with: String + "All values starting with the given string." + fonts_starts_with: String id: ID "All values containing the given string." id_contains: ID @@ -3076,7 +3469,121 @@ input ThemeWhereInput { "All values starting with the given string." id_starts_with: ID logo: AssetWhereInput + onBackgroundColor: String + "All values containing the given string." + onBackgroundColor_contains: String + "All values ending with the given string." + onBackgroundColor_ends_with: String + "All values that are contained in given list." + onBackgroundColor_in: [String!] + "All values that are not equal to given value." + onBackgroundColor_not: String + "All values not containing the given string." + onBackgroundColor_not_contains: String + "All values not ending with the given string" + onBackgroundColor_not_ends_with: String + "All values that are not contained in given list." + onBackgroundColor_not_in: [String!] + "All values not starting with the given string." + onBackgroundColor_not_starts_with: String + "All values starting with the given string." + onBackgroundColor_starts_with: String + onErrorColor: String + "All values containing the given string." + onErrorColor_contains: String + "All values ending with the given string." + onErrorColor_ends_with: String + "All values that are contained in given list." + onErrorColor_in: [String!] + "All values that are not equal to given value." + onErrorColor_not: String + "All values not containing the given string." + onErrorColor_not_contains: String + "All values not ending with the given string" + onErrorColor_not_ends_with: String + "All values that are not contained in given list." + onErrorColor_not_in: [String!] + "All values not starting with the given string." + onErrorColor_not_starts_with: String + "All values starting with the given string." + onErrorColor_starts_with: String + onPrimaryColor: String + "All values containing the given string." + onPrimaryColor_contains: String + "All values ending with the given string." + onPrimaryColor_ends_with: String + "All values that are contained in given list." + onPrimaryColor_in: [String!] + "All values that are not equal to given value." + onPrimaryColor_not: String + "All values not containing the given string." + onPrimaryColor_not_contains: String + "All values not ending with the given string" + onPrimaryColor_not_ends_with: String + "All values that are not contained in given list." + onPrimaryColor_not_in: [String!] + "All values not starting with the given string." + onPrimaryColor_not_starts_with: String + "All values starting with the given string." + onPrimaryColor_starts_with: String + onSecondaryColor: String + "All values containing the given string." + onSecondaryColor_contains: String + "All values ending with the given string." + onSecondaryColor_ends_with: String + "All values that are contained in given list." + onSecondaryColor_in: [String!] + "All values that are not equal to given value." + onSecondaryColor_not: String + "All values not containing the given string." + onSecondaryColor_not_contains: String + "All values not ending with the given string" + onSecondaryColor_not_ends_with: String + "All values that are not contained in given list." + onSecondaryColor_not_in: [String!] + "All values not starting with the given string." + onSecondaryColor_not_starts_with: String + "All values starting with the given string." + onSecondaryColor_starts_with: String + onSurfaceColor: String + "All values containing the given string." + onSurfaceColor_contains: String + "All values ending with the given string." + onSurfaceColor_ends_with: String + "All values that are contained in given list." + onSurfaceColor_in: [String!] + "All values that are not equal to given value." + onSurfaceColor_not: String + "All values not containing the given string." + onSurfaceColor_not_contains: String + "All values not ending with the given string" + onSurfaceColor_not_ends_with: String + "All values that are not contained in given list." + onSurfaceColor_not_in: [String!] + "All values not starting with the given string." + onSurfaceColor_not_starts_with: String + "All values starting with the given string." + onSurfaceColor_starts_with: String primaryColor: String + primaryColorVariant1: String + "All values containing the given string." + primaryColorVariant1_contains: String + "All values ending with the given string." + primaryColorVariant1_ends_with: String + "All values that are contained in given list." + primaryColorVariant1_in: [String!] + "All values that are not equal to given value." + primaryColorVariant1_not: String + "All values not containing the given string." + primaryColorVariant1_not_contains: String + "All values not ending with the given string" + primaryColorVariant1_not_ends_with: String + "All values that are not contained in given list." + primaryColorVariant1_not_in: [String!] + "All values not starting with the given string." + primaryColorVariant1_not_starts_with: String + "All values starting with the given string." + primaryColorVariant1_starts_with: String "All values containing the given string." primaryColor_contains: String "All values ending with the given string." @@ -3110,6 +3617,63 @@ input ThemeWhereInput { publishedAt_not: DateTime "All values that are not contained in given list." publishedAt_not_in: [DateTime!] + secondaryColor: String + secondaryColorVariant1: String + "All values containing the given string." + secondaryColorVariant1_contains: String + "All values ending with the given string." + secondaryColorVariant1_ends_with: String + "All values that are contained in given list." + secondaryColorVariant1_in: [String!] + "All values that are not equal to given value." + secondaryColorVariant1_not: String + "All values not containing the given string." + secondaryColorVariant1_not_contains: String + "All values not ending with the given string" + secondaryColorVariant1_not_ends_with: String + "All values that are not contained in given list." + secondaryColorVariant1_not_in: [String!] + "All values not starting with the given string." + secondaryColorVariant1_not_starts_with: String + "All values starting with the given string." + secondaryColorVariant1_starts_with: String + "All values containing the given string." + secondaryColor_contains: String + "All values ending with the given string." + secondaryColor_ends_with: String + "All values that are contained in given list." + secondaryColor_in: [String!] + "All values that are not equal to given value." + secondaryColor_not: String + "All values not containing the given string." + secondaryColor_not_contains: String + "All values not ending with the given string" + secondaryColor_not_ends_with: String + "All values that are not contained in given list." + secondaryColor_not_in: [String!] + "All values not starting with the given string." + secondaryColor_not_starts_with: String + "All values starting with the given string." + secondaryColor_starts_with: String + surfaceColor: String + "All values containing the given string." + surfaceColor_contains: String + "All values ending with the given string." + surfaceColor_ends_with: String + "All values that are contained in given list." + surfaceColor_in: [String!] + "All values that are not equal to given value." + surfaceColor_not: String + "All values not containing the given string." + surfaceColor_not_contains: String + "All values not ending with the given string" + surfaceColor_not_ends_with: String + "All values that are not contained in given list." + surfaceColor_not_in: [String!] + "All values not starting with the given string." + surfaceColor_not_starts_with: String + "All values starting with the given string." + surfaceColor_starts_with: String updatedAt: DateTime "All values greater than the given value." updatedAt_gt: DateTime @@ -3146,23 +3710,23 @@ input VersionWhereInput { } -scalar RGBAHue - -"Slate-compatible RichText AST" -scalar RichTextAST +"A date string, such as 2007-12-03 (YYYY-MM-DD), compliant with ISO 8601 standard for representation of dates using the Gregorian calendar." +scalar Date -scalar RGBATransparency +"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the date-timeformat outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representationof dates and times using the Gregorian calendar." +scalar DateTime scalar Hex -"A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the date-timeformat outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representationof dates and times using the Gregorian calendar." -scalar DateTime +"Raw JSON value" +scalar Json "The Long scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1." scalar Long -"Raw JSON value" -scalar Json +scalar RGBAHue -"A date string, such as 2007-12-03 (YYYY-MM-DD), compliant with ISO 8601 standard for representation of dates using the Gregorian calendar." -scalar Date +scalar RGBATransparency + +"Slate-compatible RichText AST" +scalar RichTextAST diff --git a/src/components/ComponentTemplate.tsx b/src/components/ComponentTemplate.tsx index 2d7575935..cd2fe5e56 100644 --- a/src/components/ComponentTemplate.tsx +++ b/src/components/ComponentTemplate.tsx @@ -7,8 +7,6 @@ type Props = {} /** * This component is a template meant to be duplicated to quickly get started with new React components. - * - * @param props */ const ComponentTemplate: React.FunctionComponent = (props): JSX.Element => { return ( diff --git a/src/components/appBootstrap/BrowserPageBootstrap.tsx b/src/components/appBootstrap/BrowserPageBootstrap.tsx index 63a728b71..b57993a9e 100644 --- a/src/components/appBootstrap/BrowserPageBootstrap.tsx +++ b/src/components/appBootstrap/BrowserPageBootstrap.tsx @@ -8,9 +8,14 @@ import { AmplitudeClient } from 'amplitude-js'; import { useTheme } from 'emotion-theming'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import useCustomer from '../../hooks/useCustomer'; +import useDataset from '../../hooks/useDataset'; +import amplitudeContext from '../../stores/amplitudeContext'; +import { cypressContext } from '../../stores/cypressContext'; import userConsentContext from '../../stores/userConsentContext'; import { userSessionContext } from '../../stores/userSessionContext'; -import { Theme } from '../../types/data/Theme'; +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'; @@ -18,7 +23,6 @@ import { OnlyBrowserPageProps } from '../../types/pageProps/OnlyBrowserPageProps import { UserConsent } from '../../types/UserConsent'; import { UserSemiPersistentSession } from '../../types/UserSemiPersistentSession'; import { getAmplitudeInstance } from '../../utils/analytics/amplitude'; - import initCookieConsent, { getUserConsent } from '../../utils/cookies/cookieConsent'; import UniversalCookiesManager from '../../utils/cookies/UniversalCookiesManager'; import { @@ -27,7 +31,10 @@ import { } from '../../utils/iframe'; import { configureSentryUser } from '../../utils/monitoring/sentry'; import { detectLightHouse } from '../../utils/quality/lighthouse'; -import { detectCypress } from '../../utils/testing/cypress'; +import { + CYPRESS_WINDOW_NS, + detectCypress, +} from '../../utils/testing/cypress'; const fileLabel = 'components/appBootstrap/BrowserPageBootstrap'; const logger = createLogger({ @@ -55,6 +62,8 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => locale, } = pageProps; const { t, i18n } = useTranslation(); + const dataset = useDataset(); + const customer: Customer = useCustomer(); const isInIframe: boolean = isRunningInIframe(); const iframeReferrer: string = getIframeReferrer(); const cookiesManager: UniversalCookiesManager = new UniversalCookiesManager(); // On browser, we can access cookies directly (doesn't need req/res or page context) @@ -67,7 +76,7 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => cookiesManager, userSession, }; - const theme = useTheme(); + const theme = useTheme(); const isCypressRunning = detectCypress(); const isLightHouseRunning = detectLightHouse(); @@ -95,6 +104,7 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => initCookieConsent({ allowedPages: [ // We only allow it on those pages to avoid display that boring popup on every page `${window.location.origin}/${locale}/terms`, + `${window.location.origin}/${locale}/privacy`, `${window.location.origin}/${locale}/examples/built-in-features/cookies-consent`, ], amplitudeInstance, @@ -104,6 +114,14 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => userConsent, }); + // XXX Inject data so that Cypress can use them to run dynamic tests. + // Those data mustn't be sensitive. They'll be available in the DOM, no matter the stage of the app. + // This is needed to run E2E tests that are specific to a customer. (dynamic testing) + window[CYPRESS_WINDOW_NS] = { + dataset, + customer, + }; + // In non-production stages, bind some utilities to the browser's DOM, for ease of quick testing if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { window['amplitudeInstance'] = amplitudeInstance; @@ -158,15 +176,19 @@ const BrowserPageBootstrap = (props: BrowserPageBootstrapProps): JSX.Element => // So, userProperties defined here then it will NOT be applied until the NEXT Amplitude event and this is likely gonna cause analytics issues // userProperties={{}} > - - - - - + + + + + + + + + ); diff --git a/src/components/appBootstrap/MultiversalAppBootstrap.tsx b/src/components/appBootstrap/MultiversalAppBootstrap.tsx index 4818a33e5..a79e34f95 100644 --- a/src/components/appBootstrap/MultiversalAppBootstrap.tsx +++ b/src/components/appBootstrap/MultiversalAppBootstrap.tsx @@ -3,18 +3,31 @@ import { isBrowser } from '@unly/utils'; import { createLogger } from '@unly/utils-simple-logger'; import { ThemeProvider } from 'emotion-theming'; import { i18n } from 'i18next'; +import find from 'lodash.find'; +import includes from 'lodash.includes'; import isEmpty from 'lodash.isempty'; +import size from 'lodash.size'; import React, { useState } from 'react'; import ErrorPage from '../../pages/_error'; +import { NO_AUTO_PREVIEW_MODE_KEY } from '../../pages/api/preview'; import customerContext from '../../stores/customerContext'; +import datasetContext from '../../stores/datasetContext'; import i18nContext from '../../stores/i18nContext'; import previewModeContext from '../../stores/previewModeContext'; -import { Theme } from '../../types/data/Theme'; +import quickPreviewContext from '../../stores/quickPreviewContext'; +import { Customer } from '../../types/data/Customer'; +import { CustomerTheme } from '../../types/data/CustomerTheme'; import { MultiversalAppBootstrapProps } from '../../types/nextjs/MultiversalAppBootstrapProps'; import { SSGPageProps } from '../../types/pageProps/SSGPageProps'; import { SSRPageProps } from '../../types/pageProps/SSRPageProps'; -import { stringifyQueryParameters } from '../../utils/app/router'; +import { + i18nRedirect, + stringifyQueryParameters, +} from '../../utils/app/router'; import { initCustomerTheme } from '../../utils/data/theme'; +import deserializeSafe from '../../utils/graphCMSDataset/deserializeSafe'; +import { GraphCMSDataset } from '../../utils/graphCMSDataset/GraphCMSDataset'; +import { DEFAULT_LOCALE } from '../../utils/i18n/i18n'; import i18nextLocize from '../../utils/i18n/i18nextLocize'; import { configureSentryI18n } from '../../utils/monitoring/sentry'; import { @@ -25,6 +38,7 @@ import { detectLightHouse } from '../../utils/quality/lighthouse'; import { detectCypress } from '../../utils/testing/cypress'; import Loader from '../animations/Loader'; import DefaultErrorLayout from '../errors/DefaultErrorLayout'; +import ErrorDebug from '../errors/ErrorDebug'; import BrowserPageBootstrap, { BrowserPageBootstrapProps } from './BrowserPageBootstrap'; import MultiversalGlobalStyles from './MultiversalGlobalStyles'; import ServerPageBootstrap, { ServerPageBootstrapProps } from './ServerPageBootstrap'; @@ -75,34 +89,86 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } const { - customer, + serializedDataset, i18nTranslations, lang, locale, }: SSGPageProps | SSRPageProps = pageProps; configureSentryI18n(lang, locale); - let preview, - previewData; + if (typeof serializedDataset !== 'string') { + return ( + + ); + } + + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production') { + // XXX It's too cumbersome to do proper typings when type changes + // The "customer" was forwarded as a JSON-ish string (using Flatten) in order to avoid circular dependencies issues (SSG/SSR) + // It now being converted back into an object to be actually usable on all pages + // eslint-disable-next-line no-console + console.debug('pageProps.serializedDataset length (bytes)', (serializedDataset as unknown as string)?.length); + // console.debug('serializedDataset', serializedDataset); + } + + const dataset: GraphCMSDataset = deserializeSafe(serializedDataset); + const customer: Customer = find(dataset, { __typename: 'Customer' }) as Customer; + let availableLanguages: string[] = customer?.availableLanguages; + + if (isEmpty(availableLanguages)) { + // If no language have been set, apply default (fallback) + // XXX Applying proper default is critical to avoid an infinite loop + availableLanguages = [DEFAULT_LOCALE]; + } + + if (process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && isBrowser()) { + // eslint-disable-next-line no-console + console.debug(`pageProps.dataset (${size(Object.keys(dataset))} items)`, dataset); + // eslint-disable-next-line no-console + console.debug('dataset.customer', customer); + } + + // If the locale used to display the page isn't available for this customer + // TODO This should be replaced by something better, ideally the pages for non-available locales shouldn't be generated at all and then this wouldn't be needed + if (!includes(availableLanguages, locale) && isBrowser()) { + // Then redirect to the same page using another locale (using the first available locale) + // XXX Be extra careful with this kind of redirects based on remote data! + // It's easy to create an infinite redirect loop when the data aren't shaped as expected. + i18nRedirect(availableLanguages?.[0] || DEFAULT_LOCALE, router); + return null; + } + + let isPreviewModeEnabled; + let previewData; + let isQuickPreviewPage; if ('preview' in pageProps) { // SSG - preview = pageProps.preview; - previewData = pageProps.previewData; + isPreviewModeEnabled = pageProps?.preview; + previewData = pageProps?.previewData; if (isBrowser()) { const queryParameters: string = stringifyQueryParameters(router); const isCypressRunning = detectCypress(); const isLightHouseRunning = detectLightHouse(); + const noAutoPreviewMode = new URLSearchParams(window?.location?.search)?.get(NO_AUTO_PREVIEW_MODE_KEY) === 'true'; // XXX If we are running in staging stage and the preview mode is not enabled, then we force enable it // We do this to enforce the staging stage is being used as a "preview environment" so it satisfies our publication workflow // If we're running in development, then we don't enforce anything // If we're running in production, then we force disable the preview mode, because we don't want to allow it in production // XXX Also, don't enable preview mode when Cypress or LightHouse are running to avoid bad performances - if (process.env.NEXT_PUBLIC_APP_STAGE === 'staging' && !preview && !isCypressRunning && !isLightHouseRunning) { + if (process.env.NEXT_PUBLIC_APP_STAGE === 'staging' && !isPreviewModeEnabled && !isCypressRunning && !isLightHouseRunning && !noAutoPreviewMode) { startPreviewMode(queryParameters); - } else if (process.env.NEXT_PUBLIC_APP_STAGE === 'production' && preview) { + } else if (process.env.NEXT_PUBLIC_APP_STAGE === 'production' && isPreviewModeEnabled) { logger.error('Preview mode is not allowed in production, but was detected as enabled. It will now be disabled by force.'); Sentry.captureMessage('Preview mode is not allowed in production, but was detected as enabled. It will now be disabled by force.', Sentry.Severity.Error); stopPreviewMode(queryParameters); @@ -110,8 +176,9 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } } else { // SSR - preview = false; + isPreviewModeEnabled = false; previewData = null; + isQuickPreviewPage = pageProps?.isQuickPreviewPage; } if (!customer || !i18nTranslations || !lang || !locale) { @@ -154,7 +221,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele } const i18nextInstance: i18n = i18nextLocize(lang, i18nTranslations); // Apply i18next configuration with Locize backend - const theme: Theme = initCustomerTheme(customer); + const customerTheme: CustomerTheme = initCustomerTheme(customer); /* * We split the rendering between server and browser @@ -184,7 +251,7 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele ...pageProps, i18nextInstance, isSSGFallbackInitialBuild: isSSGFallbackInitialBuild, - theme, + customerTheme, }, }; } else { @@ -195,34 +262,38 @@ const MultiversalAppBootstrap: React.FunctionComponent = (props): JSX.Ele ...pageProps, i18nextInstance, isSSGFallbackInitialBuild: isSSGFallbackInitialBuild, - theme, + customerTheme, }, }; } return ( - - - - {/* XXX Global styles that applies to all pages go there */} - - - - { - isBrowser() ? ( - - ) : ( - - ) - } - - - - + + + + + + {/* XXX Global styles that applies to all pages go there */} + + + + { + isBrowser() ? ( + + ) : ( + + ) + } + + + + + + ); } else { diff --git a/src/components/appBootstrap/MultiversalGlobalStyles.tsx b/src/components/appBootstrap/MultiversalGlobalStyles.tsx index b847a0735..3eba23446 100644 --- a/src/components/appBootstrap/MultiversalGlobalStyles.tsx +++ b/src/components/appBootstrap/MultiversalGlobalStyles.tsx @@ -2,15 +2,12 @@ import { css, Global, } from '@emotion/core'; - -import { - NRN_DEFAULT_FONT, - NRN_DEFAULT_SECONDARY_COLOR, -} from '../../constants'; -import { Theme } from '../../types/data/Theme'; +import React from 'react'; +import { NRN_DEFAULT_FALLBACK_FONTS } from '../../constants'; +import { CustomerTheme } from '../../types/data/CustomerTheme'; type Props = { - theme: Theme; + customerTheme: CustomerTheme; } /** @@ -18,28 +15,36 @@ type Props = { * - universally (browser + server) * - globally (applied to all pages), through Layouts * - * XXX Note that primaryColor, primaryAltColor and secondaryColor don't necessarily follow best practices regarding colors management. - * I personally recommend to take a look at https://material.io/design/color/the-color-system.html#color-theme-creation, those guidelines may fit your use-case - * Don't hesitate to share best practices around those, this does the job for simple use-cases - * * @param props */ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Element => { - const { theme } = props; - const { primaryColor } = theme; - const primaryAltColor = primaryColor; // Helper for "primary alternative color", for customers with 2 primary colors (currently unused) - const secondaryColor = NRN_DEFAULT_SECONDARY_COLOR; - const primaryFont = NRN_DEFAULT_FONT; // You could allow custom font per customer from Theme.font (that's what we do in our proprietary app) + const { customerTheme } = props; + const { + primaryColor, + primaryColorVariant1, + onPrimaryColor, + secondaryColor, + secondaryColorVariant1, + onSecondaryColor, + backgroundColor, + onBackgroundColor, + surfaceColor, + onSurfaceColor, + errorColor, + onErrorColor, + fonts, + } = customerTheme; return ( = (props): JSX.Ele &.wf-active { body.nrn { * { - font-family: "${primaryFont}", sans-serif !important; + font-family: "${fonts}", "${NRN_DEFAULT_FALLBACK_FONTS}" !important; } } } @@ -56,18 +61,17 @@ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Ele // Only applied to the main application body.nrn { - background-color: #f5f5f5; + background-color: ${backgroundColor}; - #__next{ - min-height: 100vh; - display: flex; - flex-direction: column; - justify-content: space-between; + .page-container { + background-color: ${backgroundColor}; + min-height: 400px; // Avoids sidebar to display on top of the footer low height pages + + @media (max-width: 991.98px) { + min-height: 300px; + } } - } - // Applied to all containers marked with ".nrn" - XXX could be grouped with the other one above? - .nrn { .container { justify-content: center; text-align: center; @@ -83,110 +87,92 @@ const MultiversalGlobalStyles: React.FunctionComponent = (props): JSX.Ele margin-bottom: 30px; } - // ----------- Utilities ----------- - - b, .b, strong { - color: ${primaryColor}; - font-weight: bold; + #__next { + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: space-between; } + } - a { - color: ${primaryColor} !important; - } + // 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 { + // ----------- Application-wide custom elements ----------- - .page-container { - background-color: #f5f5f5; - min-height: 400px; // Avoids sidebar to display on top of the footer low height pages - @media (max-width: 991.98px) { - min-height: 300px; - } - } + // ----------- Color system utilities ----------- - .center { - text-align: center; - } + a { + color: ${primaryColor}; - .center-block { - text-align: center; - margin: auto; + &:hover { + color: ${primaryColorVariant1}; + } } - .pcolor, [class*="primary_color"] { + .pcolor, + .primary-color, + [class*="primary_color"] { color: ${primaryColor}; fill: ${primaryColor}; // For SVG } - .pacolor { - color: ${primaryAltColor}; + .pbgcolor, + .primary-background-color { + background-color: ${primaryColor}; + color: ${onPrimaryColor}; } - .scolor { + .scolor, + .secondary-color, + [class*="secondary_color"] { color: ${secondaryColor}; + fill: ${secondaryColor}; // For SVG } - .pbgcolor { - background-color: ${primaryColor}; + .sbgcolor, + .secondary-background-color { + background-color: ${secondaryColor}; + color: ${onSecondaryColor}; } - .pabgcolor { - background-color: ${primaryAltColor}; - } + // ----------- Shortcuts utilities ----------- - .sbgcolor { - background-color: ${secondaryColor}; + .b, + .bold { + font-weight: bold; } - .btn-border{ - background-color: transparent; - color: ${primaryColor}; - border: 1.5px solid ${primaryColor}; - border-radius: 30px; - margin: 5px; - padding: 5px 12px 5px 12px; - white-space: nowrap; - display: inline-block; // Necessary so that and + Learn more about the Vercel cloud platform - + Learn how to configure and use Vercel - + Learn why we chose Vercel @@ -54,13 +55,13 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "env and stages" concept - + Learn how to configure Vercel secrets, using the CLI - + Learn more about their usage and differences
@@ -77,7 +78,7 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "tenancy" concept and what MST means
@@ -91,13 +92,13 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "CI/CD" concept - + Learn how to setup CI/CD - + See how to bypass automated CI/CD and deploy manually
@@ -111,13 +112,13 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "i18n" concept - + Learn how to use the "Locize" vendor - + See usage examples
@@ -131,13 +132,13 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "Monitoring" concept - + Learn how to use the "Sentry" vendor - + See usage examples
@@ -151,13 +152,13 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "GraphQL" concept - + Learn how to use the "GraphCMS" vendor - + See usage examples
@@ -171,10 +172,10 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn how to use the "Emotion" library - + See usage examples
@@ -188,10 +189,10 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "Cookie consent" library - + Learn more about user consent and its impact on analytics
@@ -205,13 +206,13 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "Analytics" concept - + Learn how to use the "Amplitude" vendor - + See usage examples
@@ -225,10 +226,10 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + Learn more about the "Testing" concept - + Learn how to use the "Cypress" library (E2E)
@@ -242,10 +243,10 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + See all available FA icons - + See usage examples
@@ -259,10 +260,10 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + See all available animations - + See usage examples
@@ -276,10 +277,10 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + See all available Reactstrap components - + See components examples
@@ -293,13 +294,27 @@ const BuiltInFeaturesSection: React.FunctionComponent = (props): JSX.Elem
- + 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
diff --git a/src/components/doc/BuiltInUtilitiesSection.tsx b/src/components/doc/BuiltInUtilitiesSection.tsx index 158489ace..a4c34b0ea 100644 --- a/src/components/doc/BuiltInUtilitiesSection.tsx +++ b/src/components/doc/BuiltInUtilitiesSection.tsx @@ -9,6 +9,7 @@ import { } from 'reactstrap'; import I18nLink from '../i18n/I18nLink'; +import Btn from '../utils/Btn'; import Cards from '../utils/Cards'; import ExternalLink from '../utils/ExternalLink'; import DocSection from './DocSection'; @@ -33,7 +34,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -47,7 +48,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -61,7 +62,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -75,7 +76,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -89,7 +90,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See how errors are handled
@@ -103,7 +104,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -117,7 +118,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -131,7 +132,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -145,7 +146,7 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + See usage examples
@@ -159,10 +160,10 @@ const BuiltInUtilitiesSection: React.FunctionComponent = (props): JSX.Ele
- + Learn how to use the React Profiler - + See usage examples
diff --git a/src/components/doc/ExternalFeaturesSection.tsx b/src/components/doc/ExternalFeaturesSection.tsx index 626f1272b..3b695bedb 100644 --- a/src/components/doc/ExternalFeaturesSection.tsx +++ b/src/components/doc/ExternalFeaturesSection.tsx @@ -8,6 +8,7 @@ import { CardText, CardTitle, } from 'reactstrap'; +import Btn from '../utils/Btn'; import Cards from '../utils/Cards'; import ExternalLink from '../utils/ExternalLink'; @@ -57,7 +58,7 @@ const ExternalFeaturesSection: React.FunctionComponent = (props): JSX.Ele
- + Go to GraphCMS
diff --git a/src/components/doc/NativeFeaturesSection.tsx b/src/components/doc/NativeFeaturesSection.tsx index c8356021e..1e6f7bf01 100644 --- a/src/components/doc/NativeFeaturesSection.tsx +++ b/src/components/doc/NativeFeaturesSection.tsx @@ -9,6 +9,7 @@ import { CardTitle, } from 'reactstrap'; import I18nLink from '../i18n/I18nLink'; +import Btn from '../utils/Btn'; import Cards from '../utils/Cards'; import ExternalLink from '../utils/ExternalLink'; import DocSection from './DocSection'; @@ -54,10 +55,10 @@ const NativeFeaturesSection: React.FunctionComponent = (props): JSX.Eleme
- + Learn more about getServerSideProps - + Example with getServerSideProps
@@ -90,10 +91,10 @@ const NativeFeaturesSection: React.FunctionComponent = (props): JSX.Eleme
- + Learn more about getStaticProps - + Example with getStaticProps
@@ -121,7 +122,7 @@ const NativeFeaturesSection: React.FunctionComponent = (props): JSX.Eleme
- + Learn more about getStaticProps with fallback option = (props): JSX.Eleme albumId: 1, }} > - + Example with getStaticProps and fallback
@@ -154,10 +155,10 @@ const NativeFeaturesSection: React.FunctionComponent = (props): JSX.Eleme
- + Learn more about getStaticProps with revalidate option - + Example with getStaticProps and revalidate
@@ -228,10 +229,10 @@ const NativeFeaturesSection: React.FunctionComponent = (props): JSX.Eleme
- + Learn more about "optional catch-all routes" native feature - + See usage examples
diff --git a/src/components/errors/ErrorDebug.tsx b/src/components/errors/ErrorDebug.tsx index 2da6f8cb3..030925061 100644 --- a/src/components/errors/ErrorDebug.tsx +++ b/src/components/errors/ErrorDebug.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/core'; import * as React from 'react'; +import { Fragment } from 'react'; type Props = { error?: Error; @@ -59,11 +60,11 @@ const ErrorDebug = (props: Props): JSX.Element => { { context && ( - <> + Error additional context:
{stringifiedContext}
- +
) } diff --git a/src/components/i18n/I18nBtnChangeLocale.tsx b/src/components/i18n/I18nBtnChangeLocale.tsx index dd13647e2..2598d5d07 100644 --- a/src/components/i18n/I18nBtnChangeLocale.tsx +++ b/src/components/i18n/I18nBtnChangeLocale.tsx @@ -1,22 +1,17 @@ -import { css } from '@emotion/core'; -import { useTheme } from 'emotion-theming'; import startsWith from 'lodash.startswith'; import { NextRouter, useRouter, } from 'next/router'; import React from 'react'; -import { Button } from 'reactstrap'; import useI18n, { I18n } from '../../hooks/useI18n'; -import { Theme } from '../../types/data/Theme'; import { i18nRedirect } from '../../utils/app/router'; import { LANG_FR } from '../../utils/i18n/i18n'; -import EnglishFlag from '../svg/EnglishFlag'; -import FrenchFlag from '../svg/FrenchFlag'; -import Tooltip from '../utils/Tooltip'; +import ToggleLanguagesButton from '../utils/ToggleLanguagesButton'; type Props = { + id: string; onClick?: (any) => void; } @@ -36,17 +31,17 @@ const defaultHandleClick = (currentLocale: string, router): void => { /** * Button that changes the current language used by the application * - * XXX Current UI is not particularly pretty, if you want to contribute to improve this particular component, you're welcome! - * * @param props */ const I18nBtnChangeLocale: React.FunctionComponent = (props): JSX.Element => { + const { + id, + } = props; let { onClick, } = props; const { lang, locale }: I18n = useI18n(); const router: NextRouter = useRouter(); - const { primaryColor } = useTheme(); if (!onClick) { onClick = (): void => { @@ -55,59 +50,12 @@ const I18nBtnChangeLocale: React.FunctionComponent = (props): JSX.Element } return ( - + isChecked={lang === LANG_FR} + /> ); }; diff --git a/src/components/pageLayouts/DefaultLayout.tsx b/src/components/pageLayouts/DefaultLayout.tsx index cb6ae25de..b407e7c6e 100644 --- a/src/components/pageLayouts/DefaultLayout.tsx +++ b/src/components/pageLayouts/DefaultLayout.tsx @@ -32,7 +32,7 @@ export type SidebarProps = { type Props = { children: React.ReactNode; - headProps: HeadProps; + headProps?: HeadProps; pageName: string; Sidebar?: React.FunctionComponent; } & SoftPageProps; @@ -88,9 +88,9 @@ const DefaultLayout: React.FunctionComponent = (props): JSX.Element => { { // XXX You may want to enable preview mode during non-production stages only - // process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && ( - - // ) + process.env.NEXT_PUBLIC_APP_STAGE !== 'production' && ( + + ) } { diff --git a/src/components/pageLayouts/Footer.tsx b/src/components/pageLayouts/Footer.tsx index 3db940371..a46e1fe93 100644 --- a/src/components/pageLayouts/Footer.tsx +++ b/src/components/pageLayouts/Footer.tsx @@ -2,15 +2,14 @@ import { css } from '@emotion/core'; import { useTheme } from 'emotion-theming'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { - Col, - Row, -} from 'reactstrap'; +import { NRN_CO_BRANDING_LOGO_URL } from '../../constants'; import useCustomer from '../../hooks/useCustomer'; import useUserSession, { UserSession } from '../../hooks/useUserSession'; +import { CSSStyles } from '../../types/CSSStyles'; +import { Asset } from '../../types/data/Asset'; import { Customer } from '../../types/data/Customer'; -import { Theme } from '../../types/data/Theme'; +import { CustomerTheme } from '../../types/data/CustomerTheme'; import { SIZE_XS } from '../../utils/assets/logo'; import GraphCMSAsset from '../assets/GraphCMSAsset'; import Logo from '../assets/Logo'; @@ -18,138 +17,177 @@ import I18nBtnChangeLocale from '../i18n/I18nBtnChangeLocale'; import I18nLink from '../i18n/I18nLink'; import DisplayOnBrowserMount from '../rehydration/DisplayOnBrowserMount'; -type Props = {}; +type Props = { + style?: CSSStyles; +}; -const Footer: React.FunctionComponent = () => { +const Footer: React.FunctionComponent = (props) => { + const { + style, + } = props; const { t } = useTranslation(); const { deviceId }: UserSession = useUserSession(); const customer: Customer = useCustomer(); - const theme = useTheme(); - const { primaryColor, logo } = theme; + const { availableLanguages } = customer; + const shouldDisplayI18nButton = availableLanguages?.length > 1; + const theme = useTheme(); + const { backgroundColor, onBackgroundColor, logo } = theme; const logoSizesMultipliers = [ { size: SIZE_XS, multiplier: 1, // We wanna keep the logos in the footer big and visible even on small devices, we've got enough space }, ]; - - // Resolve values, handle multiple fallback levels const copyrightOwner = customer?.label; const currentYear = (new Date()).getFullYear(); return ( ); }; diff --git a/src/components/pageLayouts/Head.tsx b/src/components/pageLayouts/Head.tsx index a4b8afbe4..42d5596d6 100644 --- a/src/components/pageLayouts/Head.tsx +++ b/src/components/pageLayouts/Head.tsx @@ -2,15 +2,18 @@ import { isBrowser } from '@unly/utils'; import NextHead from 'next/head'; import React from 'react'; -import { NRN_DEFAULT_SERVICE_LABEL } from '../../constants'; +import { + NRN_DEFAULT_FONT, + NRN_DEFAULT_SERVICE_LABEL, +} from '../../constants'; import { I18nLocale } from '../../types/i18n/I18nLocale'; import { SUPPORTED_LOCALES } from '../../utils/i18n/i18n'; export type HeadProps = { - title?: string; - description?: string; - url?: string; - ogImage?: string; + seoTitle?: string; + seoDescription?: string; + seoUrl?: string; + seoImage?: string; favicon?: string; additionalContent?: React.ReactElement; } @@ -21,18 +24,18 @@ export type HeadProps = { * https://github.com/vercel/next.js#populating-head */ const Head: React.FunctionComponent = (props): JSX.Element => { - const defaultDescription = 'Flexible production-grade boilerplate with Next.js 9, Vercel and TypeScript. Includes multiple opt-in presets using GraphQL, Analytics, CSS-in-JS, Monitoring, End-to-end testing, Internationalization, CI/CD and SaaS B2B multiple single-tenants (monorepo) support'; - const defaultOGURL = 'https://github.com/UnlyEd/next-right-now'; - const defaultOGImage = 'https://storage.googleapis.com/the-funding-place/assets/images/Logo_TFP_quadri_horizontal.svg'; - const defaultFavicon = 'https://storage.googleapis.com/the-funding-place/assets/images/default_favicon.ico'; + const defaultDescription = 'Flexible production-grade boilerplate with Next.js 9, Vercel and TypeScript. Includes multiple opt-in presets using Airtable, Analytics, CSS-in-JS, Monitoring, End-to-end testing, Internationalization, CI/CD and SaaS B2B multiple single-tenants (monorepo) support'; + const defaultMetaURL = 'https://github.com/UnlyEd/next-right-now'; + const defaultMetaImage = ''; + const defaultFavicon = ''; const { - title = NRN_DEFAULT_SERVICE_LABEL, - description = defaultDescription, - ogImage = defaultOGURL, - url = defaultOGImage, + seoTitle = NRN_DEFAULT_SERVICE_LABEL, + seoDescription = defaultDescription, + seoImage = defaultMetaImage, + seoUrl = defaultMetaURL, favicon = defaultFavicon, - additionalContent, + additionalContent = null, } = props; if (isBrowser()) { @@ -45,7 +48,7 @@ const Head: React.FunctionComponent = (props): JSX.Element => { // XXX See https://github.com/typekit/webfontloader#custom WebFontLoader.load({ custom: { - families: ['neuzeit-grotesk'], + families: [NRN_DEFAULT_FONT], urls: ['/static/fonts/NeuzeitGrotesk/font.css'], }, }); @@ -54,10 +57,10 @@ const Head: React.FunctionComponent = (props): JSX.Element => { return ( - {title} + {seoTitle} {/**/} @@ -85,16 +88,16 @@ const Head: React.FunctionComponent = (props): JSX.Element => { }) } - - + + - + - - + + diff --git a/src/components/pageLayouts/Nav.tsx b/src/components/pageLayouts/Nav.tsx index 268c64ecb..2c6d8d8e4 100644 --- a/src/components/pageLayouts/Nav.tsx +++ b/src/components/pageLayouts/Nav.tsx @@ -2,6 +2,7 @@ import { Amplitude } from '@amplitude/react-amplitude'; import { css } from '@emotion/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; +import { useTheme } from 'emotion-theming'; import kebabCase from 'lodash.kebabcase'; import map from 'lodash.map'; import { @@ -22,10 +23,11 @@ import { Row, UncontrolledDropdown, } from 'reactstrap'; - import useI18n, { I18n } from '../../hooks/useI18n'; import customerContext, { CustomerContext } from '../../stores/customerContext'; import { LogEvent } from '../../types/Amplitude'; +import { Asset } from '../../types/data/Asset'; +import { CustomerTheme } from '../../types/data/CustomerTheme'; import { SidebarLink } from '../../types/SidebarLink'; import { isActive, @@ -43,7 +45,7 @@ type Props = {}; const Nav: React.FunctionComponent = () => { const { t } = useTranslation(); const router: NextRouter = useRouter(); - const { theme }: CustomerContext = React.useContext(customerContext); + const theme = useTheme(); const { locale }: I18n = useI18n(); const { primaryColor, logo } = theme; @@ -147,10 +149,9 @@ const Nav: React.FunctionComponent = () => {
diff --git a/src/components/pageLayouts/PreviewModeBanner.tsx b/src/components/pageLayouts/PreviewModeBanner.tsx index c35086110..2ad820e4b 100644 --- a/src/components/pageLayouts/PreviewModeBanner.tsx +++ b/src/components/pageLayouts/PreviewModeBanner.tsx @@ -1,24 +1,25 @@ import { css } from '@emotion/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTheme } from 'emotion-theming'; import { NextRouter, useRouter, } from 'next/router'; -import React from 'react'; +import React, { Fragment } from 'react'; import { Trans, useTranslation, } from 'react-i18next'; -import { Button } from 'reactstrap'; -import Alert from 'reactstrap/lib/Alert'; 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 Tooltip from '../utils/Tooltip'; type Props = {} @@ -54,37 +55,50 @@ const ExplanationTooltipOverlay: React.FunctionComponent = (): JSX.Element => { * @param props */ const PreviewModeBanner: React.FunctionComponent = (props): JSX.Element => { - const { preview }: PreviewMode = usePreviewMode(); + const { isPreviewModeEnabled }: PreviewMode = usePreviewMode(); const router: NextRouter = useRouter(); const queryParameters: string = stringifyQueryParameters(router); const { t } = useTranslation(); + const { + secondaryColor, secondaryColorVariant1, onSecondaryColor, + } = useTheme(); if (process.env.NEXT_PUBLIC_APP_STAGE === 'production') { return null; } return ( - = (props): JSX.Element = `} > { - preview ? ( -
- + isPreviewModeEnabled ? ( + +
+
{t(`previewModeBanner.previewModeEnabledTitle`, `Vous êtes sur l'environnement de prévisualisation`)}   = (props): JSX.Element = > - - - { - process.env.NEXT_PUBLIC_APP_STAGE === 'development' && ( - - - - ) - } -
+ + ) + } +
+
) : ( -
- + +
+
{t(`previewModeBanner.previewModeDisabledTitle`, `L'environnement de prévisualisation est désactivé`)}   = (props): JSX.Element = > - - - { - process.env.NEXT_PUBLIC_APP_STAGE === 'development' && ( - - - - ) - } -
+ + ) + } +
+
) } - +
); }; diff --git a/src/components/pageLayouts/QuickPreviewBanner.tsx b/src/components/pageLayouts/QuickPreviewBanner.tsx new file mode 100644 index 000000000..6b8305701 --- /dev/null +++ b/src/components/pageLayouts/QuickPreviewBanner.tsx @@ -0,0 +1,154 @@ +import { css } from '@emotion/core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { createLogger } from '@unly/utils-simple-logger'; +import { useTheme } from 'emotion-theming'; +import { + NextRouter, + useRouter, +} from 'next/router'; +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'; + +const fileLabel = 'components/pageLayouts/QuickPreviewBanner'; +const logger = createLogger({ + label: fileLabel, +}); + +type Props = { + ExplanationTooltipOverlay?: React.FunctionComponent; + LeftActions?: React.FunctionComponent; + quickPreviewTitle?: string; +}; + +/** + * Banner used by all Quick Preview pages. + * + * A Quick Preview page is meant to be used from an external CMS (e.g: Stacker). + * The banner is meant to provide additional capabilities that focus on improving usability, within the said CMS. + * + * The left side of the banner provides page-specific actions. + * The right side of the banner provides common actions available across all quick previews, such as: + * - Flag to change current language + * - Refresh button to force refresh the page + */ +const QuickPreviewBanner: React.FunctionComponent = (props): JSX.Element => { + const { + ExplanationTooltipOverlay = null, + LeftActions = null, + quickPreviewTitle, + } = props; + const { + secondaryColor, secondaryColorVariant1, onSecondaryColor, + } = useTheme(); + const { t } = useTranslation(); + const router: NextRouter = useRouter(); + const customer: Customer = useCustomer(); + const { availableLanguages } = customer; + const shouldDisplayI18nButton = availableLanguages?.length > 1; + + return ( +
+
+
+ { + LeftActions && ( + + ) + } +
+
+ { + quickPreviewTitle ? ( +

+ ) : ( +

+ {t('quickPreviewBanner.quickPreviewTitle', `Aperçu rapide`)} +

+ ) + } + { + !!ExplanationTooltipOverlay && ( + +   + } + placement={'bottom'} + > + + + + ) + } +
+
+
+ router.reload()} + > + + {t('quickPreviewBanner.refresh', `Actualiser`)} + +
+ { + shouldDisplayI18nButton && ( +
+ + +
+ ) + } +
+
+
+ ); +}; + +export default QuickPreviewBanner; diff --git a/src/components/rehydration/DisplayOnBrowserMount.tsx b/src/components/rehydration/DisplayOnBrowserMount.tsx index 6aa6968cb..7d444e7e7 100644 --- a/src/components/rehydration/DisplayOnBrowserMount.tsx +++ b/src/components/rehydration/DisplayOnBrowserMount.tsx @@ -1,6 +1,8 @@ +import size from 'lodash.size'; import some from 'lodash.some'; import React, { DependencyList, + Fragment, useState, } from 'react'; @@ -56,7 +58,7 @@ const DisplayOnBrowserMount: React.FunctionComponent = (props) => { deps = [], } = props; // If any dep isn't defined, then it will render "null" first, and then trigger a re-render - const isAnyDepsNullish = deps.length ? + const isAnyDepsNullish = size(deps) ? // If any deps was provided, check if any is null-ish some(deps, (dependency: any): boolean => { return dependency === null || typeof dependency === 'undefined'; @@ -76,9 +78,9 @@ const DisplayOnBrowserMount: React.FunctionComponent = (props) => { } return ( - <> + {children} - + ); }; diff --git a/src/components/svg/Animated3Dots.tsx b/src/components/svg/Animated3Dots.tsx new file mode 100644 index 000000000..41c4e04a4 --- /dev/null +++ b/src/components/svg/Animated3Dots.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +/** + * An animated composant featuring 3 animated dots "...". + * + * Each dot is animated separately, in alternation. + * Requires animate.css library. + * + * @see https://animate.style + */ +const Animated3Dots = (props): JSX.Element => { + return ( + + + + + + ); +}; + +export default Animated3Dots; diff --git a/src/components/svg/AnimatedTextBubble.tsx b/src/components/svg/AnimatedTextBubble.tsx new file mode 100644 index 000000000..4ecae232b --- /dev/null +++ b/src/components/svg/AnimatedTextBubble.tsx @@ -0,0 +1,55 @@ +import { css } from '@emotion/core'; +import { useTheme } from 'emotion-theming'; +import React from 'react'; + +import { Theme } from '../../types/data/Theme'; + +const AnimatedTextBubble = props => { + const theme: Theme = useTheme(); + const { surfaceColor } = theme; + return ( +
+ + + + + + +
+ ); +}; + +export default AnimatedTextBubble; diff --git a/src/components/svg/EnglishFlag.tsx b/src/components/svg/EnglishFlag.tsx index e2adc7f1c..ad054f533 100644 --- a/src/components/svg/EnglishFlag.tsx +++ b/src/components/svg/EnglishFlag.tsx @@ -1,18 +1,16 @@ -import { css } from '@emotion/core'; import React from 'react'; -const EnglishFlag = props => { +type Props = {} & React.SVGProps; + +const EnglishFlag = (props: Props): JSX.Element => { return ( @@ -29,5 +27,4 @@ const EnglishFlag = props => { ); }; -type Props = {} & React.SVGProps; export default EnglishFlag; diff --git a/src/components/svg/FrenchFlag.tsx b/src/components/svg/FrenchFlag.tsx index 4e22b7b86..586d39bf7 100644 --- a/src/components/svg/FrenchFlag.tsx +++ b/src/components/svg/FrenchFlag.tsx @@ -3,8 +3,9 @@ import React from 'react'; const FrenchFlag = props => { 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/CircleBtn.tsx b/src/components/utils/CircleBtn.tsx new file mode 100644 index 000000000..37fd1e81b --- /dev/null +++ b/src/components/utils/CircleBtn.tsx @@ -0,0 +1,101 @@ +import { css } from '@emotion/core'; +import { useTheme } from 'emotion-theming'; +import React from 'react'; +import { CustomerTheme } from '../../types/data/CustomerTheme'; +import { ReactDivProps } from '../../types/react/ReactDivProps'; +import { + ComponentThemeMode, + resolveThemedComponentColors, + ThemedComponentProps, +} from '../../utils/theming/themedComponentColors'; + +type Props = { + children: React.ReactNode; + margin?: string; +} & ReactDivProps & ThemedComponentProps; + +/** + * Displays a 1-3 characters, or an icon, within a perfect circle. + * + * Themed component. + * + * Used by: + * - Contact slider button + * - Report button + * - Size/number of label + */ +const CircleBtn: React.FunctionComponent = (props): JSX.Element => { + const { + children, + margin = '0', + mode = 'primary-reverse' as ComponentThemeMode, + transparent, + ...rest + } = props; + const customerTheme = useTheme(); + const { + color, + backgroundColor, + borderColor, + } = resolveThemedComponentColors(customerTheme, mode, transparent); + + return ( +
); } catch (e) { @@ -96,9 +105,9 @@ const Markdown: React.FunctionComponent = (props): JSX.Element => { }); return ( - <> + {text} - + ); } }; diff --git a/src/components/utils/SpoilerButton.tsx b/src/components/utils/SpoilerButton.tsx new file mode 100644 index 000000000..781a9bd3a --- /dev/null +++ b/src/components/utils/SpoilerButton.tsx @@ -0,0 +1,62 @@ +import classnames from 'classnames'; +import React, { useState } from 'react'; + +type Props = { + previewElement: JSX.Element; + spoilerElement: JSX.Element; + className?: string; + href: string; + target?: string; + id?: string; +} + +/** + * 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 + */ +const SpoilerButton: React.FunctionComponent = (props): JSX.Element => { + const { + previewElement, + spoilerElement, + className, + href, + target = '_blank', + id = null, + } = props; + const [isPreview, setIsPreview] = useState(true); + + if (isPreview) { + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions + setIsPreview(false)} + > + {previewElement} + + ); + } else { + return ( + + {spoilerElement} + + ); + } +}; + +export default SpoilerButton; diff --git a/src/components/utils/Stamp.tsx b/src/components/utils/Stamp.tsx new file mode 100644 index 000000000..d8b32d62d --- /dev/null +++ b/src/components/utils/Stamp.tsx @@ -0,0 +1,83 @@ +import { css } from '@emotion/core'; +import { useTheme } from 'emotion-theming'; +import React, { ReactNode } from 'react'; +import { CustomerTheme } from '../../types/data/CustomerTheme'; + +type Props = { + children: ReactNode; +} + +/** + * Displays a stamp, which is similar to a tag, an etiquette. + * + * @param props + */ +export const Stamp: React.FunctionComponent = (props): JSX.Element => { + const { + children, + ...rest + } = props; + const { secondaryColorVariant1, secondaryColor } = useTheme(); + + return ( +
+
+ + {children} + +
+
+ ); +}; + +export default Stamp; diff --git a/src/components/utils/ToggleButton.tsx b/src/components/utils/ToggleButton.tsx new file mode 100644 index 000000000..3ea2049df --- /dev/null +++ b/src/components/utils/ToggleButton.tsx @@ -0,0 +1,309 @@ +import { css } from '@emotion/core'; +import React from 'react'; + +type Props = { + id: string; + mode?: 'light' | 'ios' | 'skewed' | 'flat' | 'flip'; + flipModeOptions?: { + useBackgroundColor?: boolean; + }; + valueOn: any; + valueOff: any; + isChecked?: boolean; +} & React.HTMLProps; + +/** + * Toggle button (as checkbox) between two possible values. + * + * @param props + * @see https://codepen.io/mallendeo/pen/eLIiG + */ +const ToggleButton: React.FunctionComponent = (props): JSX.Element => { + const { + id, + mode = 'flip', + flipModeOptions = { + useBackgroundColor: false, + }, + valueOn, + valueOff, + isChecked, + ...rest + } = props; + + return ( + + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + ); +}; + +export default ToggleButton; diff --git a/src/components/utils/ToggleLanguagesButton.tsx b/src/components/utils/ToggleLanguagesButton.tsx new file mode 100644 index 000000000..ce370d949 --- /dev/null +++ b/src/components/utils/ToggleLanguagesButton.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import ToggleButton from './ToggleButton'; + +type Props = { + 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, ...")` + isChecked?: boolean; +} & React.HTMLProps; + +/** + * Button that toggles between two language flags. + * + * @param props + */ +const ToggleLanguagesButton: React.FunctionComponent = (props): JSX.Element => { + const frenchFlagSvg = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 35 17' width='20px' %3E%3Cstyle%3E %7B '.FrenchFlag_svg__st4,.FrenchFlag_svg__st6%7Bdisplay:inline;fill:%23fff%7D.FrenchFlag_svg__st6%7Bfill:%23002496%7D' %7D %3C/style%3E%3Cg id='FrenchFlag_svg__Calque_2'%3E%3Cpath fill='%23002496' d='M0-.089h10v17.178H0z' /%3E%3Cpath fill='%23fff' d='M10-.089h10v17.178H10z' /%3E%3Cpath fill='%23ed2839' d='M20-.089h10v17.178H20z' /%3E%3C/g%3E%3C/svg%3E")`; + const englishFlagSvg = `url("data:image/svg+xml,%3Csvg id='Calque_1' data-name='Calque 1' xmlns='http://www.w3.org/2000/svg' width='20px' viewBox='0 0 20 10'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23c82531;%7D.cls-2%7Bfill:%23fefefe;%7D.cls-3%7Bfill:%23252e64;%7D%3C/style%3E%3C/defs%3E%3Cg id='Y5ra3v.tif'%3E%3Cpath class='cls-1' d='M0,6V4H8.91C9,4,9,4,9,3.91V0h2V3.9c0,.08,0,.1.1.1H20V6H11.09C11,6,11,6,11,6.09V10H9V6.1C9,6,9,6,8.9,6Z'/%3E%3Cpath class='cls-2' d='M9,0V3.91C9,4,9,4,8.91,4H0V3.33H4.41L0,1.12V.75l.1,0,5,2.5a.33.33,0,0,0,.17,0h1.3s0,0,.07,0l-.07,0L.1.05A.18.18,0,0,1,0,0H2.25l0,0,6,3s0,0,.06,0V0Z'/%3E%3Cpath class='cls-2' d='M11,10V6.09C11,6,11,6,11.09,6H20v.67H15.59L20,8.88v.37l-.1,0-5-2.5a.33.33,0,0,0-.17,0H13.35l.09,0L19.9,10A.18.18,0,0,1,20,10H17.75l-.05,0-6-3s0,0-.06,0v3Z'/%3E%3Cpath class='cls-2' d='M20,4H11.1C11,4,11,4,11,3.9V0h.67V3.05l.07,0L16.88.44,17.75,0h.75l-.08.05L12,3.28l-.08,0h1.37a.33.33,0,0,0,.17,0L19.3.35c.23-.12.46-.22.68-.35,0,0,0,.05,0,.07,0,.35,0,.7,0,1.05l-4.41,2.2H20Z'/%3E%3Cpath class='cls-2' d='M0,6H8.9C9,6,9,6,9,6.1V10H8.33V7L8.26,7,3.13,9.56c-.3.14-.59.29-.88.44H1.5L1.58,10,8.05,6.72l.08,0H6.76a.33.33,0,0,0-.17,0L.7,9.65C.47,9.77.24,9.87,0,10c0,0,0-.05,0-.07,0-.35,0-.7,0-1l4.41-2.2H0Z'/%3E%3Cpath class='cls-3' d='M8.33,0V3s0,0-.06,0l-6-3,0,0Z'/%3E%3Cpath class='cls-3' d='M17.75,0l-.87.44L11.74,3l-.07,0V0Z'/%3E%3Cpath class='cls-3' d='M2.25,10c.29-.15.58-.3.88-.44L8.26,7l.07,0V10Z'/%3E%3Cpath class='cls-3' d='M11.67,10V7s0,0,.06,0l6,3,.05,0Z'/%3E%3Cpath class='cls-1' d='M20,0c-.22.13-.45.23-.68.35L13.41,3.29a.33.33,0,0,1-.17,0H11.87l.08,0L18.42.05l.08,0Z'/%3E%3Cpath class='cls-1' d='M0,10c.22-.13.45-.23.68-.35L6.59,6.71a.33.33,0,0,1,.17,0H8.13l-.08,0L1.58,10,1.5,10Z'/%3E%3Cpath class='cls-3' d='M0,1.12l4.41,2.2H0Z'/%3E%3Cpath class='cls-3' d='M20,3.33H15.59L20,1.12Z'/%3E%3Cpath class='cls-3' d='M0,6.67H4.41L0,8.88Z'/%3E%3Cpath class='cls-3' d='M20,8.88l-4.41-2.2H20Z'/%3E%3Cpath class='cls-1' d='M0,0A.18.18,0,0,0,.1.05L6.57,3.29l.07,0s0,0-.07,0H5.27a.33.33,0,0,1-.17,0L.1.79,0,.75c0-.23,0-.47,0-.7C0,0,0,0,0,0Z'/%3E%3Cpath class='cls-1' d='M20,10A.18.18,0,0,0,19.9,10L13.44,6.72l-.09,0h1.38a.33.33,0,0,1,.17,0l5,2.5.1,0c0,.23,0,.47,0,.7C20,10,20,10,20,10Z'/%3E%3C/g%3E%3C/svg%3E")`; + const { + id, + flag1 = frenchFlagSvg, + flag2 = englishFlagSvg, + isChecked, + ...rest + } = props; + + return ( + + ); +}; + +export default ToggleLanguagesButton; diff --git a/src/constants.ts b/src/constants.ts index 542efc774..020a7fd83 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,11 +1,132 @@ -import { Theme } from './types/data/Theme'; +import { CustomerTheme } from './types/data/CustomerTheme'; +import { resolveVariantColor } from './utils/colors'; -export const NRN_DEFAULT_SERVICE_LABEL = 'Next Right Now!'; +export const NRN_DEFAULT_SERVICE_LABEL = process.env.NEXT_PUBLIC_APP_STAGE === 'production' ? 'Next Right Now' : `[${process.env.NEXT_PUBLIC_APP_STAGE === 'staging' ? 'Preview' : 'Dev'}] Next Right Now`; + +/** + * Co-branding logo displayed in the footer ("powered by Unly") + */ +export const NRN_CO_BRANDING_LOGO_URL = '/static/images/LOGO_Powered_by_UNLY_BLACK_BLUE.svg'; + +/** + * Fallback fonts used until our own fonts have been loaded by the browser. + * Should only use native fonts that are installed on all devices by default. + * + * @see https://www.w3schools.com/cssref/css_websafe_fonts.asp + */ +export const NRN_DEFAULT_FALLBACK_FONTS = 'sans-serif'; + +/** + * Font used once our font have been loaded by the browser. + */ export const NRN_DEFAULT_FONT = 'neuzeit-grotesk'; -export const NRN_DEFAULT_SECONDARY_COLOR = '#fff'; -export const NRN_DEFAULT_THEME: Theme = { - primaryColor: 'blue', +/** + * Theme applied by default when no theme is defined. + * Will be used on a variable-by-variable basis based on what's configured on the CMS for the customer. + * + * Applied through "src/utils/airtableSchema/airtableSchema.ts" default value transformations. + * + * Strongly inspired from Material Design Color System. + * @see The below documentation comes from https://material.io/design/color/the-color-system.html + */ +export const NRN_DEFAULT_THEME: Omit & { + primaryColorVariant1: (primaryColor: string) => string; + secondaryColorVariant1: (primaryColor: string) => string; +} = { + /** + * A primary color is the color displayed most frequently across your app's screens and components. + * + */ + primaryColor: '#0028FF', + + /** + * Primary color first variant, meant to highlight interactive elements using the primary color (hover, etc.). + */ + primaryColorVariant1: resolveVariantColor, + + /** + * Color applied to text, icons, surfaces displayed on top of the primary color. + */ + onPrimaryColor: '#fff', + + /** + * A secondary color provides more ways to accent and distinguish your product. + * Having a secondary color is optional, and should be applied sparingly to accent select parts of your UI. + * If you don’t have a secondary color, your primary color can also be used to accent elements. + * + * The secondary color should be the same as the primary color, when no particular secondary color is being used. + * + * Secondary colors are best for: + * - Floating action buttons + * - Selection controls, like sliders and switches + * - Highlighting selected text + * - Progress bars + * - Links and headlines + */ + secondaryColor: '#000', + + /** + * Secondary color first variant, meant to highlight interactive elements using the primary color (hover, etc.). + */ + secondaryColorVariant1: resolveVariantColor, + + /** + * Color applied to text, icons, surfaces displayed on top of the secondary color. + */ + onSecondaryColor: '#fff', + + /** + * Background colors don’t represent brand. + * + * The background color appears behind top-level content. + * + * Used by/for: + * - All pages background + */ + backgroundColor: '#f4f4f4', + + /** + * Color applied to text, icons, surfaces displayed on top of the background color. + */ + onBackgroundColor: '#000', + + /** + * Surface colors don’t represent brand. + * + * Surface colors affect surfaces of components, such as cards, sheets, and menus. + */ + surfaceColor: '#fff', + + /** + * Color applied to text, icons, surfaces displayed on top of the surface color. + */ + onSurfaceColor: '#000', + + /** + * Error colors don’t represent brand. + * + * Error color indicates errors in components, such as invalid text in a text field. + * The baseline error color is #B00020. + */ + errorColor: '#FFE0E0', + + /** + * Color applied to text, icons, surfaces displayed on top of the error color. + */ + onErrorColor: '#FE6262', + + /** + * Logo displayed on the top footer. + */ + logo: null, + + /** + * Fonts used by the application. + * + * XXX At the moment, we don't allow the customer to define its own font, even though it's part of the customer theme. + */ + fonts: NRN_DEFAULT_FONT, }; export const GITHUB_API_BASE_URL = 'https://api.github.com/'; diff --git a/src/gql/pages/terms.ts b/src/gql/pages/terms.ts index 3b8cf6d00..8af98306e 100644 --- a/src/gql/pages/terms.ts +++ b/src/gql/pages/terms.ts @@ -10,7 +10,7 @@ export const TERMS_PAGE_QUERY = gql` }){ id label - terms { + termsDescription { html } } diff --git a/src/hooks/useAmplitude.tsx b/src/hooks/useAmplitude.tsx new file mode 100644 index 000000000..3c3815684 --- /dev/null +++ b/src/hooks/useAmplitude.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import amplitudeContext, { AmplitudeContext } from '../stores/amplitudeContext'; + +export type Amplitude = AmplitudeContext + +/** + * Hook to access amplitude data + * + * Uses amplitudeContext internally (provides an identical API) + * + * This hook should be used by components in favor of amplitudeContext directly, + * because it grants higher flexibility if you ever need to change the implementation (e.g: use something else than React.Context, like Redux/MobX/Recoil) + * + * @see https://slides.com/djanoskova/react-context-api-create-a-reusable-snackbar#/11 + */ +const useAmplitude = (): Amplitude => { + return React.useContext(amplitudeContext); +}; + +export default useAmplitude; diff --git a/src/hooks/useCustomer.tsx b/src/hooks/useCustomer.tsx index dd510f27a..972253703 100644 --- a/src/hooks/useCustomer.tsx +++ b/src/hooks/useCustomer.tsx @@ -5,7 +5,7 @@ import { Customer } from '../types/data/Customer'; /** * Hook to access customer data * - * The customer data are pre-fetched, either during SSR or SSG and are not meant to be fetched or modified by the app (they're kinda read-only) + * The customer data are pre-fetched, either during SSR or SSG and are not meant to be mutated afterwards (they're kinda read-only) * Thus, it's fine to use React Context for this kind of usage. * * XXX If you need to store data that are meant to be updated (e.g: through forms) then using React Context is a very bad idea! diff --git a/src/hooks/useCypress.tsx b/src/hooks/useCypress.tsx new file mode 100644 index 000000000..c0300bbe1 --- /dev/null +++ b/src/hooks/useCypress.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import cypressContext, { CypressContext } from '../stores/cypressContext'; + +/** + * Hook to access Cypress data. + */ +const useCypress = (): CypressContext => { + return React.useContext(cypressContext); +}; + +export default useCypress; diff --git a/src/hooks/useDataset.tsx b/src/hooks/useDataset.tsx new file mode 100644 index 000000000..e5e637893 --- /dev/null +++ b/src/hooks/useDataset.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import datasetContext, { DatasetContext } from '../stores/datasetContext'; + +/** + * Hook to access dataset data + * + * The dataset data are pre-fetched, either during SSR or SSG and are not meant to be mutated afterwards (they're kinda read-only) + * Thus, it's fine to use React Context for this kind of usage. + * + * XXX If you need to store data that are meant to be updated (e.g: through forms) then using React Context is a very bad idea! + * If you don't know why, you should read more about it. + * Long story short, React Context is better be used with data that doesn't mutate, like theme/localisation + * Read more at https://medium.com/swlh/recoil-another-react-state-management-library-97fc979a8d2b + * If you need to handle a global state that changes over time, your should rather use a dedicated library (opinionated: I'd probably use Recoil) + * + * Uses datasetContext internally (provides an identical API) + * + * This hook should be used by components in favor of datasetContext directly, + * because it grants higher flexibility if you ever need to change the implementation (e.g: use something else than React.Context, like Redux/MobX/Recoil) + * + * @see https://slides.com/djanoskova/react-context-api-create-a-reusable-snackbar#/11 + */ +const useDataset = (): DatasetContext => { + return React.useContext(datasetContext); +}; + +export default useDataset; diff --git a/src/hooks/useQuickPreview.tsx b/src/hooks/useQuickPreview.tsx new file mode 100644 index 000000000..5b9a693ae --- /dev/null +++ b/src/hooks/useQuickPreview.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import quickPreviewContext, { QuickPreviewContext } from '../stores/quickPreviewContext'; + +export type QuickPreview = QuickPreviewContext + +/** + * Hook to access quick-preview data + * + * Uses quickPreviewContext internally (provides an identical API) + * + * This hook should be used by components in favor of quickPreviewContext directly, + * because it grants higher flexibility if you ever need to change the implementation (e.g: use something else than React.Context, like Redux/MobX/Recoil) + * + * @see https://nextjs.org/docs/advanced-features/preview-mode + */ +const useQuickPreview = (): QuickPreviewContext => { + return React.useContext(quickPreviewContext); +}; + +export default useQuickPreview; diff --git a/src/i18nConfig.js b/src/i18nConfig.js index 14a5e7058..dbfd2cfef 100644 --- a/src/i18nConfig.js +++ b/src/i18nConfig.js @@ -5,7 +5,6 @@ const defaultLocale = 'en'; const supportedLocales = [ - { name: 'fr-FR', lang: 'fr' }, { name: 'fr', lang: 'fr' }, { name: 'en-US', lang: 'en' }, { name: 'en', lang: 'en' }, @@ -14,6 +13,7 @@ const supportedLanguages = supportedLocales.map((item) => { return item.lang; }); +// XXX Available through utils/i18n/i18n module.exports = { defaultLocale: defaultLocale, supportedLocales: supportedLocales, diff --git a/src/middlewares/localeMiddleware.ts b/src/middlewares/localeMiddleware.ts index f048da984..336860c54 100644 --- a/src/middlewares/localeMiddleware.ts +++ b/src/middlewares/localeMiddleware.ts @@ -1,4 +1,5 @@ import { createLogger } from '@unly/utils-simple-logger'; +import size from 'lodash.size'; import { NextApiRequest, NextApiResponse, @@ -31,7 +32,7 @@ export const localeMiddleware = async (req: NextApiRequest, res: NextApiResponse const detections: string[] = acceptLanguageHeaderLookup(req) || []; let localeFound; // Will contain the most preferred browser locale (e.g: fr-FR, fr, en-US, en, etc.) - if (detections && detections.length) { + if (detections && !!size(detections)) { detections.forEach((language) => { if (localeFound || typeof language !== 'string') return; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 93fb0ed50..5a944b797 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { createLogger } from '@unly/utils-simple-logger'; import { GetStaticProps, @@ -9,9 +10,11 @@ import { useRouter, } from 'next/router'; // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars -import React from 'react'; - +import React, { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; +import I18nLink from '../components/i18n/I18nLink'; import DefaultLayout from '../components/pageLayouts/DefaultLayout'; +import Btn from '../components/utils/Btn'; import { CommonServerSideParams } from '../types/nextjs/CommonServerSideParams'; import { SoftPageProps } from '../types/pageProps/SoftPageProps'; import { SSGPageProps } from '../types/pageProps/SSGPageProps'; @@ -20,7 +23,7 @@ import { LANG_EN, LANG_FR, } from '../utils/i18n/i18n'; -import { getExamplesCommonStaticProps } from '../utils/nextjs/SSG'; +import { getCommonStaticProps } from '../utils/nextjs/SSG'; const fileLabel = 'pages/404'; const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript-eslint/no-unused-vars @@ -35,7 +38,7 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation */ -export const getStaticProps: GetStaticProps = getExamplesCommonStaticProps; +export const getStaticProps: GetStaticProps = getCommonStaticProps; /** * SSG pages are first rendered by the server (during static bundling) @@ -49,25 +52,25 @@ type Props = {} & SoftPageProps; const Fr404 = (): JSX.Element => { return ( - <> +

Page non trouvée

La page que vous recherchez n'existe pas

- +
); }; const En404 = (): JSX.Element => { return ( - <> +

Page not found

The page you're looking for doesn't exist

- +
); }; @@ -83,6 +86,7 @@ const En404 = (): JSX.Element => { * @see https://nextjs.org/docs/advanced-features/custom-error-page#404-page */ const NotFound404Page: NextPage = (props): JSX.Element => { + const { t } = useTranslation(); const router: NextRouter = useRouter(); const locale = router?.asPath?.split('/')?.[1] || DEFAULT_LOCALE; const lang: string = locale.split('-')?.[0]; @@ -107,7 +111,7 @@ const NotFound404Page: NextPage = (props): JSX.Element => { {...props} pageName={'404'} headProps={{ - title: '404 Not Found - Next Right Now', + seoTitle: '404 Not Found', }} >
= (props): JSX.Element => { text-align: center; `} > + {content} + + + + + {t('404.indexPage.link', 'Accueil')} + +
); diff --git a/src/pages/[locale]/examples/built-in-features/analytics.tsx b/src/pages/[locale]/examples/built-in-features/analytics.tsx index b51387f35..455602bd2 100644 --- a/src/pages/[locale]/examples/built-in-features/analytics.tsx +++ b/src/pages/[locale]/examples/built-in-features/analytics.tsx @@ -67,7 +67,7 @@ const ExampleAnalyticsPage: NextPage = (props): JSX.Element => { {...props} pageName={'analytics'} headProps={{ - title: 'Analytics examples - Next Right Now', + seoTitle: 'Analytics examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/animations.tsx b/src/pages/[locale]/examples/built-in-features/animations.tsx index 6dc4d4091..17e7b796c 100644 --- a/src/pages/[locale]/examples/built-in-features/animations.tsx +++ b/src/pages/[locale]/examples/built-in-features/animations.tsx @@ -59,7 +59,7 @@ const ExampleAnimationPage: NextPage = (props): JSX.Element => { {...props} pageName={'animations'} headProps={{ - title: 'Animations examples - Next Right Now', + seoTitle: 'Animations examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/cookies-consent.tsx b/src/pages/[locale]/examples/built-in-features/cookies-consent.tsx index 138c0bfe6..44a424337 100644 --- a/src/pages/[locale]/examples/built-in-features/cookies-consent.tsx +++ b/src/pages/[locale]/examples/built-in-features/cookies-consent.tsx @@ -63,7 +63,7 @@ const ExampleCookiesConsentPage: NextPage = (props): JSX.Element => { {...props} pageName={'cookies-consent'} headProps={{ - title: 'Cookies consent examples - Next Right Now', + seoTitle: 'Cookies consent examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/css-in-js.tsx b/src/pages/[locale]/examples/built-in-features/css-in-js.tsx index 53c1de830..2a5cfa3ac 100644 --- a/src/pages/[locale]/examples/built-in-features/css-in-js.tsx +++ b/src/pages/[locale]/examples/built-in-features/css-in-js.tsx @@ -59,7 +59,7 @@ const ExampleCssInJsPage: NextPage = (props): JSX.Element => { {...props} pageName={'css-in-js'} headProps={{ - title: 'CSS-in-JS examples - Next Right Now', + seoTitle: 'CSS-in-JS examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/docs-site.tsx b/src/pages/[locale]/examples/built-in-features/docs-site.tsx index a81b2feff..2734a326d 100644 --- a/src/pages/[locale]/examples/built-in-features/docs-site.tsx +++ b/src/pages/[locale]/examples/built-in-features/docs-site.tsx @@ -58,7 +58,7 @@ const DocsSiteExamplePage: NextPage = (props): JSX.Element => { {...props} pageName={'docs-site'} headProps={{ - title: 'Docs site - Next Right Now', + seoTitle: 'Docs site - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/graphql.tsx b/src/pages/[locale]/examples/built-in-features/graphql.tsx index da295d8da..0276ab215 100644 --- a/src/pages/[locale]/examples/built-in-features/graphql.tsx +++ b/src/pages/[locale]/examples/built-in-features/graphql.tsx @@ -58,7 +58,7 @@ const ExampleGraphQLPage: NextPage = (props): JSX.Element => { {...props} pageName={'graphql'} headProps={{ - title: 'GraphQL examples - Next Right Now', + seoTitle: 'GraphQL examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/hosting.tsx b/src/pages/[locale]/examples/built-in-features/hosting.tsx index 3d414db16..1bc2577a3 100644 --- a/src/pages/[locale]/examples/built-in-features/hosting.tsx +++ b/src/pages/[locale]/examples/built-in-features/hosting.tsx @@ -58,7 +58,7 @@ const HostingPage: NextPage = (props): JSX.Element => { {...props} pageName={'hosting'} headProps={{ - title: 'Hosting - Next Right Now', + seoTitle: 'Hosting - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/icons.tsx b/src/pages/[locale]/examples/built-in-features/icons.tsx index 27860923c..8f8843eca 100644 --- a/src/pages/[locale]/examples/built-in-features/icons.tsx +++ b/src/pages/[locale]/examples/built-in-features/icons.tsx @@ -59,7 +59,7 @@ const ExampleIconsPage: NextPage = (props): JSX.Element => { {...props} pageName={'icons'} headProps={{ - title: 'Icons examples - Next Right Now', + seoTitle: 'Icons examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/manual-deployments.tsx b/src/pages/[locale]/examples/built-in-features/manual-deployments.tsx index 57aff6e64..9ef97e9ba 100644 --- a/src/pages/[locale]/examples/built-in-features/manual-deployments.tsx +++ b/src/pages/[locale]/examples/built-in-features/manual-deployments.tsx @@ -58,7 +58,7 @@ const ManualDeploymentsPage: NextPage = (props): JSX.Element => { {...props} pageName={'manual-deployments'} headProps={{ - title: 'Manual-deployments examples - Next Right Now', + seoTitle: 'Manual-deployments examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/monitoring.tsx b/src/pages/[locale]/examples/built-in-features/monitoring.tsx index 9523095be..a7eb9171d 100644 --- a/src/pages/[locale]/examples/built-in-features/monitoring.tsx +++ b/src/pages/[locale]/examples/built-in-features/monitoring.tsx @@ -58,7 +58,7 @@ const ExampleMonitoringPage: NextPage = (props): JSX.Element => { {...props} pageName={'monitoring'} headProps={{ - title: 'Monitoring examples - Next Right Now', + seoTitle: 'Monitoring examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/stages-and-secrets.tsx b/src/pages/[locale]/examples/built-in-features/stages-and-secrets.tsx index d682c7db6..6753894c8 100644 --- a/src/pages/[locale]/examples/built-in-features/stages-and-secrets.tsx +++ b/src/pages/[locale]/examples/built-in-features/stages-and-secrets.tsx @@ -58,7 +58,7 @@ const StagesAndSecretsPage: NextPage = (props): JSX.Element => { {...props} pageName={'stages-and-secrets'} headProps={{ - title: 'Stages & secrets - Next Right Now', + seoTitle: 'Stages & secrets - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/static-i18n.tsx b/src/pages/[locale]/examples/built-in-features/static-i18n.tsx index 14315b7cf..a3980cbac 100644 --- a/src/pages/[locale]/examples/built-in-features/static-i18n.tsx +++ b/src/pages/[locale]/examples/built-in-features/static-i18n.tsx @@ -74,7 +74,7 @@ const ExampleStaticI18nPage: NextPage = (props): JSX.Element => { {...props} pageName={'static-i18n'} headProps={{ - title: 'Static i18n examples - Next Right Now', + seoTitle: 'Static i18n examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-features/ui-components.tsx b/src/pages/[locale]/examples/built-in-features/ui-components.tsx index 610c15e93..4c62feba1 100644 --- a/src/pages/[locale]/examples/built-in-features/ui-components.tsx +++ b/src/pages/[locale]/examples/built-in-features/ui-components.tsx @@ -63,7 +63,7 @@ const ExampleUIComponentsPage: NextPage = (props): JSX.Element => { {...props} pageName={'ui-components'} headProps={{ - title: 'UI components examples - Next Right Now', + seoTitle: 'UI components examples - Next Right Now', }} Sidebar={BuiltInFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/api.tsx b/src/pages/[locale]/examples/built-in-utilities/api.tsx index 1a465a517..492a87144 100644 --- a/src/pages/[locale]/examples/built-in-utilities/api.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/api.tsx @@ -58,7 +58,7 @@ const ApiPage: NextPage = (props): JSX.Element => { {...props} pageName={'api'} headProps={{ - title: 'Api examples - Next Right Now', + seoTitle: 'Api examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/bundle-analysis.tsx b/src/pages/[locale]/examples/built-in-utilities/bundle-analysis.tsx index d0ab475cc..401b24712 100644 --- a/src/pages/[locale]/examples/built-in-utilities/bundle-analysis.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/bundle-analysis.tsx @@ -57,7 +57,7 @@ const AnalyseBundlePage: NextPage = (props): JSX.Element => { {...props} pageName={'bundle-analysis'} headProps={{ - title: 'Bundle analysis examples - Next Right Now', + seoTitle: 'Bundle analysis examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/errors-handling.tsx b/src/pages/[locale]/examples/built-in-utilities/errors-handling.tsx index a2a7aeef7..1321427d5 100644 --- a/src/pages/[locale]/examples/built-in-utilities/errors-handling.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/errors-handling.tsx @@ -60,7 +60,7 @@ const ErrorsHandlingPage: NextPage = (props): JSX.Element => { {...props} pageName={'errors-handling'} headProps={{ - title: 'Errors handling examples - Next Right Now', + seoTitle: 'Errors handling examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > @@ -137,7 +137,7 @@ const ErrorsHandlingPage: NextPage = (props): JSX.Element => { {...props} pageName={'page-500-error'} headProps={{ - title: 'Top-level 500 error example - Next Right Now', + seoTitle: 'Top-level 500 error example - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/hocs.tsx b/src/pages/[locale]/examples/built-in-utilities/hocs.tsx index 3f056fc0b..43896a5d3 100644 --- a/src/pages/[locale]/examples/built-in-utilities/hocs.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/hocs.tsx @@ -59,7 +59,7 @@ const HocsPage: NextPage = (props): JSX.Element => { {...props} pageName={'hocs'} headProps={{ - title: 'HOCs examples - Next Right Now', + seoTitle: 'HOCs examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/hooks.tsx b/src/pages/[locale]/examples/built-in-utilities/hooks.tsx index 3c863a532..590129c00 100644 --- a/src/pages/[locale]/examples/built-in-utilities/hooks.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/hooks.tsx @@ -60,7 +60,7 @@ const HooksPage: NextPage = (props): JSX.Element => { {...props} pageName={'hooks'} headProps={{ - title: 'Hooks examples - Next Right Now', + seoTitle: 'Hooks examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/i18nLink-component.tsx b/src/pages/[locale]/examples/built-in-utilities/i18nLink-component.tsx index 7ff408660..615d8ca51 100644 --- a/src/pages/[locale]/examples/built-in-utilities/i18nLink-component.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/i18nLink-component.tsx @@ -59,7 +59,7 @@ const ExampleI18nLinkComponentPage: NextPage = (props): JSX.Element => { {...props} pageName={'i18nLink-component'} headProps={{ - title: 'I18nLink component examples - Next Right Now', + seoTitle: 'I18nLink component examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/interactive-error.tsx b/src/pages/[locale]/examples/built-in-utilities/interactive-error.tsx index 1624f1683..fefe9efc4 100644 --- a/src/pages/[locale]/examples/built-in-utilities/interactive-error.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/interactive-error.tsx @@ -62,7 +62,7 @@ const InteractiveErrorPage: NextPage = (props): JSX.Element => { {...props} pageName={'interactive-error'} headProps={{ - title: 'Page 500 error example - Next Right Now', + seoTitle: 'Page 500 error example - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/packages-upgrade.tsx b/src/pages/[locale]/examples/built-in-utilities/packages-upgrade.tsx index e0f70d366..cdf8cdf80 100644 --- a/src/pages/[locale]/examples/built-in-utilities/packages-upgrade.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/packages-upgrade.tsx @@ -59,7 +59,7 @@ const PackagesUpgradePage: NextPage = (props): JSX.Element => { {...props} pageName={'packages-upgrade'} headProps={{ - title: 'Packages upgrade examples - Next Right Now', + seoTitle: 'Packages upgrade examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/security-audit.tsx b/src/pages/[locale]/examples/built-in-utilities/security-audit.tsx index eb1100661..6ff2677c4 100644 --- a/src/pages/[locale]/examples/built-in-utilities/security-audit.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/security-audit.tsx @@ -58,7 +58,7 @@ const SecurityAuditPage: NextPage = (props): JSX.Element => { {...props} pageName={'security-audit'} headProps={{ - title: 'Security audit examples - Next Right Now', + seoTitle: 'Security audit examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/svg-to-react.tsx b/src/pages/[locale]/examples/built-in-utilities/svg-to-react.tsx index 957cb0a4d..86a7933ac 100644 --- a/src/pages/[locale]/examples/built-in-utilities/svg-to-react.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/svg-to-react.tsx @@ -61,7 +61,7 @@ const SvgToReactPage: NextPage = (props): JSX.Element => { {...props} pageName={'svg-to-react'} headProps={{ - title: 'SVG to React examples - Next Right Now', + seoTitle: 'SVG to React examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/top-level-500-error.tsx b/src/pages/[locale]/examples/built-in-utilities/top-level-500-error.tsx index 3d2d0d06b..c160142aa 100644 --- a/src/pages/[locale]/examples/built-in-utilities/top-level-500-error.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/top-level-500-error.tsx @@ -61,7 +61,7 @@ const TopLevel500ErrorPage: NextPage = (props): JSX.Element => { {...props} pageName={'top-level-500-error'} headProps={{ - title: 'Top-level 500 error example - Next Right Now', + seoTitle: 'Top-level 500 error example - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/built-in-utilities/tracking-useless-re-renders.tsx b/src/pages/[locale]/examples/built-in-utilities/tracking-useless-re-renders.tsx index c0087768a..004892d3d 100644 --- a/src/pages/[locale]/examples/built-in-utilities/tracking-useless-re-renders.tsx +++ b/src/pages/[locale]/examples/built-in-utilities/tracking-useless-re-renders.tsx @@ -63,7 +63,7 @@ const TrackingUselessReRendersPage: NextPage = (props): JSX.Element => { {...props} pageName={'tracking-useless-re-renders'} headProps={{ - title: 'Tracking useless re-renders examples - Next Right Now', + seoTitle: 'Tracking useless re-renders examples - Next Right Now', }} Sidebar={BuiltInUtilitiesSidebar} > diff --git a/src/pages/[locale]/examples/index.tsx b/src/pages/[locale]/examples/index.tsx index 867b84f18..64ac832dc 100644 --- a/src/pages/[locale]/examples/index.tsx +++ b/src/pages/[locale]/examples/index.tsx @@ -61,7 +61,7 @@ const ExampleHomePage: NextPage = (props): JSX.Element => { {...props} pageName={'index'} headProps={{ - title: 'Homepage - Next Right Now', + seoTitle: 'Homepage - Next Right Now', }} > diff --git a/src/pages/[locale]/examples/native-features/example-optional-catch-all-routes/[[...slug]].tsx b/src/pages/[locale]/examples/native-features/example-optional-catch-all-routes/[[...slug]].tsx index 6eee41a72..9a0212112 100644 --- a/src/pages/[locale]/examples/native-features/example-optional-catch-all-routes/[[...slug]].tsx +++ b/src/pages/[locale]/examples/native-features/example-optional-catch-all-routes/[[...slug]].tsx @@ -80,7 +80,7 @@ const ExampleWithCatchAllRoutesPage: NextPage = (props): JSX.Element => { {...props} pageName={'examples'} headProps={{ - title: `Catch-all dynamic routes examples - Next Right Now`, + seoTitle: `Catch-all dynamic routes examples - Next Right Now`, }} Sidebar={NativeFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/native-features/example-with-ssg-and-fallback/[albumId].tsx b/src/pages/[locale]/examples/native-features/example-with-ssg-and-fallback/[albumId].tsx index c05f47622..53816fbcf 100644 --- a/src/pages/[locale]/examples/native-features/example-with-ssg-and-fallback/[albumId].tsx +++ b/src/pages/[locale]/examples/native-features/example-with-ssg-and-fallback/[albumId].tsx @@ -19,6 +19,7 @@ import { import NativeFeaturesSidebar from '../../../../../components/doc/NativeFeaturesSidebar'; import I18nLink from '../../../../../components/i18n/I18nLink'; import DefaultLayout from '../../../../../components/pageLayouts/DefaultLayout'; +import Btn from '../../../../../components/utils/Btn'; import ExternalLink from '../../../../../components/utils/ExternalLink'; import withApollo from '../../../../../hocs/withApollo'; import songs from '../../../../../mocks/songs'; @@ -138,7 +139,7 @@ const ExampleWithSSGAndFallbackAlbumPage: NextPage = (props): JSX.Element {...props} pageName={'example-with-ssg-and-fallback/[albumId]'} headProps={{ - title: `Album N°${albumId} (SSG, ${isSSGFallbackInitialBuild ? 'using fallback' : 'not using fallback'}) - Next Right Now`, + seoTitle: `Album N°${albumId} (SSG, ${isSSGFallbackInitialBuild ? 'using fallback' : 'not using fallback'}) - Next Right Now`, }} Sidebar={NativeFeaturesSidebar} > @@ -199,7 +200,7 @@ const ExampleWithSSGAndFallbackAlbumPage: NextPage = (props): JSX.Element albumId: id - 1, }} > - + Go to previous album ) } @@ -210,13 +211,13 @@ const ExampleWithSSGAndFallbackAlbumPage: NextPage = (props): JSX.Element albumId: id + 1, }} > - + Go to next album
- + Go to next+2 album (opens new tab)
diff --git a/src/pages/[locale]/examples/native-features/example-with-ssg-and-revalidate.tsx b/src/pages/[locale]/examples/native-features/example-with-ssg-and-revalidate.tsx index 3737b6c02..d734d6e81 100644 --- a/src/pages/[locale]/examples/native-features/example-with-ssg-and-revalidate.tsx +++ b/src/pages/[locale]/examples/native-features/example-with-ssg-and-revalidate.tsx @@ -142,7 +142,7 @@ const ProductsWithSSGPage: NextPage = (props): JSX.Element => { {...props} pageName={'examples'} headProps={{ - title: `${size(products)} products (SSG with revalidate) - Next Right Now`, + seoTitle: `${size(products)} products (SSG with revalidate) - Next Right Now`, }} Sidebar={NativeFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/native-features/example-with-ssg.tsx b/src/pages/[locale]/examples/native-features/example-with-ssg.tsx index 9be619c3c..6490d0999 100644 --- a/src/pages/[locale]/examples/native-features/example-with-ssg.tsx +++ b/src/pages/[locale]/examples/native-features/example-with-ssg.tsx @@ -136,7 +136,7 @@ const ExampleWithSSGPage: NextPage = (props): JSX.Element => { {...props} pageName={'examples'} headProps={{ - title: `${size(products)} products (SSG) - Next Right Now`, + seoTitle: `${size(products)} products (SSG) - Next Right Now`, }} Sidebar={NativeFeaturesSidebar} > diff --git a/src/pages/[locale]/examples/native-features/example-with-ssr.tsx b/src/pages/[locale]/examples/native-features/example-with-ssr.tsx index 0415af809..9e9ce1118 100644 --- a/src/pages/[locale]/examples/native-features/example-with-ssr.tsx +++ b/src/pages/[locale]/examples/native-features/example-with-ssr.tsx @@ -1,5 +1,8 @@ import { createLogger } from '@unly/utils-simple-logger'; -import { ApolloQueryResult, QueryOptions } from 'apollo-client'; +import { + ApolloQueryResult, + QueryOptions, +} from 'apollo-client'; import size from 'lodash.size'; import { GetServerSideProps, @@ -21,12 +24,15 @@ import DefaultLayout from '../../../../components/pageLayouts/DefaultLayout'; import ExternalLink from '../../../../components/utils/ExternalLink'; import { EXAMPLE_WITH_SSR_QUERY } from '../../../../gql/pages/examples/native-features/example-with-ssr'; import withApollo from '../../../../hocs/withApollo'; +import useDataset from '../../../../hooks/useDataset'; import { Customer } from '../../../../types/data/Customer'; import { Product } from '../../../../types/data/Product'; import { CommonServerSideParams } from '../../../../types/nextjs/CommonServerSideParams'; import { OnlyBrowserPageProps } from '../../../../types/pageProps/OnlyBrowserPageProps'; import { SSGPageProps } from '../../../../types/pageProps/SSGPageProps'; import { SSRPageProps } from '../../../../types/pageProps/SSRPageProps'; +import { GraphCMSDataset } from '../../../../utils/graphCMSDataset/GraphCMSDataset'; +import serializeSafe from '../../../../utils/graphCMSDataset/serializeSafe'; import { getCommonServerSideProps, GetCommonServerSidePropsResults, @@ -42,7 +48,6 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- */ type CustomPageProps = { [key: string]: any; - products: Product[]; } type GetServerSidePageProps = CustomPageProps & SSRPageProps @@ -53,7 +58,7 @@ type GetServerSidePageProps = CustomPageProps & SSRPageProps * @param context */ export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext): Promise> => { - const commonServerSideProps: GetServerSidePropsResult = await getCommonServerSideProps(context); + const commonServerSideProps: GetServerSidePropsResult> = await getCommonServerSideProps(context); if ('props' in commonServerSideProps) { @@ -103,14 +108,17 @@ export const getServerSideProps: GetServerSideProps = as customer, products, } = data || {}; // XXX Use empty object as fallback, to avoid app crash when destructuring, if no data is returned + const dataset = { + customer, + products, + }; return { // Props returned here will be available as page properties (pageProps) props: { ...pageData, apolloState: apolloClient.cache.extract(), - customer, - products, + serializedDataset: serializeSafe(dataset), }, }; } else { @@ -129,14 +137,15 @@ export const getServerSideProps: GetServerSideProps = as type Props = CustomPageProps & (SSRPageProps & SSGPageProps); const ProductsWithSSRPage: NextPage = (props): JSX.Element => { - const { products } = props; + const dataset: GraphCMSDataset = useDataset(); + const { products } = dataset; return ( diff --git a/src/pages/[locale]/pageTemplateSSG.tsx b/src/pages/[locale]/pageTemplateSSG.tsx index ebe37fe41..9720a90a0 100644 --- a/src/pages/[locale]/pageTemplateSSG.tsx +++ b/src/pages/[locale]/pageTemplateSSG.tsx @@ -9,6 +9,8 @@ import React from 'react'; import DefaultLayout from '../../components/pageLayouts/DefaultLayout'; import withApollo from '../../hocs/withApollo'; +import useCustomer from '../../hooks/useCustomer'; +import { Customer } from '../../types/data/Customer'; import { CommonServerSideParams } from '../../types/nextjs/CommonServerSideParams'; import { OnlyBrowserPageProps } from '../../types/pageProps/OnlyBrowserPageProps'; import { SSGPageProps } from '../../types/pageProps/SSGPageProps'; @@ -49,15 +51,12 @@ export const getStaticProps: GetStaticProps>; const PageTemplateSSG: NextPage = (props): JSX.Element => { - const { customer } = props; + const customer: Customer = useCustomer(); return (

This page is a template meant to be duplicated to quickly get started with new Next.js SSG pages.
diff --git a/src/pages/[locale]/pageTemplateSSR.tsx b/src/pages/[locale]/pageTemplateSSR.tsx index 97dace402..bfc08b618 100644 --- a/src/pages/[locale]/pageTemplateSSR.tsx +++ b/src/pages/[locale]/pageTemplateSSR.tsx @@ -12,11 +12,13 @@ import React from 'react'; import DefaultLayout from '../../components/pageLayouts/DefaultLayout'; import { LAYOUT_QUERY } from '../../gql/common/layoutQuery'; import withApollo from '../../hocs/withApollo'; +import useCustomer from '../../hooks/useCustomer'; import { Customer } from '../../types/data/Customer'; import { CommonServerSideParams } from '../../types/nextjs/CommonServerSideParams'; import { OnlyBrowserPageProps } from '../../types/pageProps/OnlyBrowserPageProps'; import { SSGPageProps } from '../../types/pageProps/SSGPageProps'; import { SSRPageProps } from '../../types/pageProps/SSRPageProps'; +import serializeSafe from '../../utils/graphCMSDataset/serializeSafe'; import { getCommonServerSideProps, GetCommonServerSidePropsResults, @@ -45,7 +47,7 @@ type GetServerSidePageProps = CustomPageProps & SSRPageProps * @param context */ export const getServerSideProps: GetServerSideProps = async (context: GetServerSidePropsContext): Promise> => { - const commonServerSideProps: GetServerSidePropsResult = await getCommonServerSideProps(context); + const commonServerSideProps: GetServerSidePropsResult> = await getCommonServerSideProps(context); if ('props' in commonServerSideProps) { const { @@ -77,13 +79,16 @@ export const getServerSideProps: GetServerSideProps = as const { customer, } = data || {}; // XXX Use empty object as fallback, to avoid app crash when destructuring, if no data is returned + const dataset = { + customer, + }; return { // Props returned here will be available as page properties (pageProps) props: { ...pageData, apolloState: apolloClient.cache.extract(), - customer, + serializedDataset: serializeSafe(dataset), }, }; } else { @@ -102,15 +107,12 @@ export const getServerSideProps: GetServerSideProps = as type Props = CustomPageProps & (SSRPageProps & SSGPageProps); const PageTemplateSSR: NextPage = (props): JSX.Element => { - const { customer } = props; + const customer: Customer = useCustomer(); return (

This page is a template meant to be duplicated to quickly get started with new Next.js SSR pages. diff --git a/src/pages/[locale]/privacy.tsx b/src/pages/[locale]/privacy.tsx new file mode 100644 index 000000000..3d55ae90b --- /dev/null +++ b/src/pages/[locale]/privacy.tsx @@ -0,0 +1,82 @@ +import { createLogger } from '@unly/utils-simple-logger'; +import { + GetStaticPaths, + GetStaticProps, + NextPage, +} from 'next'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars +import React from 'react'; +import DefaultLayout from '../../components/pageLayouts/DefaultLayout'; +import LegalContent from '../../components/utils/LegalContent'; +import useCustomer from '../../hooks/useCustomer'; +import { Customer } from '../../types/data/Customer'; +import { CommonServerSideParams } from '../../types/nextjs/CommonServerSideParams'; +import { OnlyBrowserPageProps } from '../../types/pageProps/OnlyBrowserPageProps'; +import { SSGPageProps } from '../../types/pageProps/SSGPageProps'; +import { AMPLITUDE_PAGES } from '../../utils/analytics/amplitude'; +import { replaceAllOccurrences } from '../../utils/js/string'; +import { + getCommonStaticPaths, + getCommonStaticProps, +} from '../../utils/nextjs/SSG'; + +const fileLabel = 'pages/[locale]/privacy'; +const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript-eslint/no-unused-vars + label: fileLabel, +}); + +/** + * Only executed on the server side at build time + * Necessary when a page has dynamic routes and uses "getStaticProps" + */ +export const getStaticPaths: GetStaticPaths = getCommonStaticPaths; + +/** + * Only executed on the server side at build time. + * + * @return Props (as "SSGPageProps") that will be passed to the Page component, as props + * + * @see https://github.com/vercel/next.js/discussions/10949#discussioncomment-6884 + * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation + */ +export const getStaticProps: GetStaticProps = getCommonStaticProps; + +/** + * SSG pages are first rendered by the server (during static bundling) + * Then, they're rendered by the client, and gain additional props (defined in OnlyBrowserPageProps) + * Because this last case is the most common (server bundle only happens during development stage), we consider it a default + * To represent this behaviour, we use the native Partial TS keyword to make all OnlyBrowserPageProps optional + * + * Beware props in OnlyBrowserPageProps are not available on the server + */ +type Props = {} & SSGPageProps>; + +/** + * Privacy page, that displays all legal-related information. + * + * Basically displays a bunch of markdown that's coming from the CMS. + */ +const PrivacyPage: NextPage = (props): JSX.Element => { + const customer: Customer = useCustomer(); + const { privacyDescription, serviceLabel } = customer || {}; + + // Replace dynamic values (like "{customerLabel}") by their actual value + const privacy = replaceAllOccurrences(privacyDescription?.html, { + serviceLabel: `**${serviceLabel}**`, + customerLabel: `**${customer?.label}**`, + }); + + return ( + +

Field's value (fetched from Airtable API), as Long text (interpreted as Markdown):

+ +
+ ); +}; + +export default (PrivacyPage); diff --git a/src/pages/[locale]/terms.tsx b/src/pages/[locale]/terms.tsx index 7dff82c26..1a7cc798b 100644 --- a/src/pages/[locale]/terms.tsx +++ b/src/pages/[locale]/terms.tsx @@ -1,4 +1,3 @@ -import { css } from '@emotion/core'; import { createLogger } from '@unly/utils-simple-logger'; import { ApolloQueryResult } from 'apollo-client'; import deepmerge from 'deepmerge'; @@ -10,25 +9,22 @@ import { } from 'next'; // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars import React from 'react'; -import { Container } from 'reactstrap'; - import DefaultLayout from '../../components/pageLayouts/DefaultLayout'; -import Code from '../../components/utils/Code'; -import Markdown from '../../components/utils/Markdown'; +import LegalContent from '../../components/utils/LegalContent'; import { TERMS_PAGE_QUERY } from '../../gql/pages/terms'; import withApollo from '../../hocs/withApollo'; import useCustomer from '../../hooks/useCustomer'; import { Customer } from '../../types/data/Customer'; import { CommonServerSideParams } from '../../types/nextjs/CommonServerSideParams'; - import { StaticPropsInput } from '../../types/nextjs/StaticPropsInput'; import { OnlyBrowserPageProps } from '../../types/pageProps/OnlyBrowserPageProps'; import { SSGPageProps } from '../../types/pageProps/SSGPageProps'; import { createApolloClient } from '../../utils/gql/graphql'; +import { AMPLITUDE_PAGES } from '../../utils/analytics/amplitude'; import { replaceAllOccurrences } from '../../utils/js/string'; import { - getExamplesCommonStaticPaths, - getExamplesCommonStaticProps, + getCommonStaticPaths, + getCommonStaticProps, } from '../../utils/nextjs/SSG'; const fileLabel = 'pages/[locale]/terms'; @@ -40,7 +36,7 @@ const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript- * Only executed on the server side at build time * Necessary when a page has dynamic routes and uses "getStaticProps" */ -export const getStaticPaths: GetStaticPaths = getExamplesCommonStaticPaths; +export const getStaticPaths: GetStaticPaths = getCommonStaticPaths; /** * Only executed on the server side at build time. @@ -51,7 +47,7 @@ export const getStaticPaths: GetStaticPaths = getExample * @see https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation */ export const getStaticProps: GetStaticProps = async (props: StaticPropsInput): Promise> => { - const commonStaticProps: GetStaticPropsResult = await getExamplesCommonStaticProps(props); + const commonStaticProps: GetStaticPropsResult = await getCommonStaticProps(props); if ('props' in commonStaticProps) { const { customerRef, gcmsLocales } = commonStaticProps.props; @@ -109,88 +105,32 @@ export const getStaticProps: GetStaticProps>; +/** + * Terms page, that displays all legal-related information. + * + * Basically displays a bunch of markdown that's coming from the CMS. + */ const TermsPage: NextPage = (props): JSX.Element => { const customer: Customer = useCustomer(); - const { theme } = customer; - const { primaryColor } = theme; - const termsRaw: string = customer?.terms?.html; + const { termsDescription, serviceLabel } = customer || {}; - // Replace dynamic values like "{customerLabel}" by their actual value - const terms = replaceAllOccurrences(termsRaw || '', { + // Replace dynamic values (like "{customerLabel}") by their actual value + const terms = replaceAllOccurrences(termsDescription?.html, { + serviceLabel: `**${serviceLabel}**`, customerLabel: `**${customer?.label}**`, }); return ( - -
-
- { - - } -
- -
- -
-

Field's value (fetched from GraphQL API), as RichText (interpreted as HTML):

- -
-
-
+
); }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a05f5762f..4a6c7379e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,7 @@ 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/ @@ -114,7 +115,7 @@ export function reportWebVitals(metrics: NextWebVitalsMetrics): void { const { name } = metrics; const count = globalWebVitalsMetric.reportedCount; globalWebVitalsMetric.metrics[name] = metrics; - const keysLength = Object.keys(globalWebVitalsMetric.metrics)?.length; + const keysLength = size(Object.keys(globalWebVitalsMetric.metrics)); // Temporise analytics API calls by waiting for at least 5 metrics to be received before sending the first report // (because 3 metrics will be received upon initial page load, and then 2 more upon first click) diff --git a/src/pages/_error.tsx b/src/pages/_error.tsx index 2d6fcdd4e..3c269a089 100644 --- a/src/pages/_error.tsx +++ b/src/pages/_error.tsx @@ -1,8 +1,7 @@ import * as Sentry from '@sentry/node'; -import get from 'lodash.get'; import { NextPageContext } from 'next'; import NextError, { ErrorProps as NextErrorProps } from 'next/error'; -import React from 'react'; +import React, { Fragment } from 'react'; export type ErrorPageProps = { err: Error; @@ -62,7 +61,7 @@ const ErrorPage = (props: ErrorPageProps): JSX.Element => { } return ( - <> + { // Render the children if provided, or return the native NextError component from Next children ? ( @@ -72,11 +71,11 @@ const ErrorPage = (props: ErrorPageProps): JSX.Element => { statusCode={statusCode} // Only display title in non-production stages, to avoid leaking debug information to end-users // When "null" is provided, it'll fallback to Next.js default message (based on the statusCode) - title={process.env.NEXT_PUBLIC_APP_STAGE !== 'production' ? get(err, 'message', null) : null} + title={process.env.NEXT_PUBLIC_APP_STAGE !== 'production' ? err?.message : null} /> ) } - + ); }; diff --git a/src/pages/api/preview.ts b/src/pages/api/preview.ts index 38e73d206..7fd1e7560 100644 --- a/src/pages/api/preview.ts +++ b/src/pages/api/preview.ts @@ -1,4 +1,5 @@ import { createLogger } from '@unly/utils-simple-logger'; +import appendQueryParameter from 'append-query'; import { NextApiRequest, NextApiResponse, @@ -12,10 +13,36 @@ const logger = createLogger({ label: fileLabel, }); -type PreviewModeAPIQuery = { - stop: string; - redirectTo: string; -} +/** + * Key to use in order to force disable "auto preview mode". + * + * If a page is loaded with noAutoPreviewMode=true in query parameter, then it won't try to enable the Preview mode, even if it's disabled. + * + * @example ?noAutoPreviewMode=true + */ +export const NO_AUTO_PREVIEW_MODE_KEY = 'noAutoPreviewMode'; + +type EndpointRequest = NextApiRequest & { + query: { + /** + * Whether to start/stop the Preview Mode. + * + * @example ?stop=true Will stop the preview mode. + * @example ?stop=false Will start the preview mode. + * @default ?stop=false + */ + stop: string; + + /** + * Url to redirect to once the preview mode has been started/stopped. + * + * @example ?redirectTo=/en + * @example ?redirectTo=/fr/solutions + * @default ?redirectTo=/ + */ + redirectTo: string; + } +}; /** * Preview Mode API @@ -33,15 +60,17 @@ type PreviewModeAPIQuery = { * @see https://nextjs.org/docs/advanced-features/preview-mode#step-1-create-and-access-a-preview-api-route * @see https://nextjs.org/docs/advanced-features/preview-mode#clear-the-preview-mode-cookies */ -export const preview = async (req: NextApiRequest, res: NextApiResponse): Promise => { +export const preview = async (req: EndpointRequest, res: NextApiResponse): Promise => { try { configureReq(req, { fileLabel }); const { stop = 'false', redirectTo = '/', - }: PreviewModeAPIQuery = req.query as PreviewModeAPIQuery; - const safeRedirectUrl = filterExternalAbsoluteUrl(redirectTo as string); + } = req.query; + // Add NO_AUTO_PREVIEW_MODE_KEY parameter to query, to avoid running into infinite loops if the Preview mode couldn't start + // Useful when the cookie created by Next.js cannot be written (Incognito mode) + const safeRedirectUrl = appendQueryParameter(filterExternalAbsoluteUrl(redirectTo as string), `${NO_AUTO_PREVIEW_MODE_KEY}=true`); // XXX We don't want to enable preview mode for the production stage, it's only allowed for non-production stages // It's allowed during development for testing purpose diff --git a/src/stores/amplitudeContext.tsx b/src/stores/amplitudeContext.tsx new file mode 100644 index 000000000..3e302d037 --- /dev/null +++ b/src/stores/amplitudeContext.tsx @@ -0,0 +1,34 @@ +import { AmplitudeClient } from 'amplitude-js'; +import React from 'react'; + +/** + * The AmplitudeContext contains amplitude-related properties + * + * @see https://stackoverflow.com/a/40076355/2391795 + * @see https://github.com/Microsoft/TypeScript/blob/ee25cdecbca49b2b5a290ecd65224f425b1d6a9c/lib/lib.es5.d.ts#L1354 + */ +export type AmplitudeContext = { + // We need to access the amplitudeInstance initialised in BrowserPageBootstrap, in other parts of the app + amplitudeInstance?: AmplitudeClient; +} + +/** + * Initial context, used by default until the Context Provider is initialised. + * + * @default Empty object, to allow for destructuring even when the context hasn't been initialised (on the server) + */ +const initialContext = {}; + +/** + * Uses native React Context API, meant to be used from hooks only, not by functional components + * + * @example Usage + * import amplitudeContext from './src/stores/amplitudeContext'; + * const { amplitudeInstance }: AmplitudeContext = React.useContext(amplitudeContext); + * + * @see https://reactjs.org/docs/context.html + * @see https://medium.com/better-programming/react-hooks-usecontext-30eb560999f for useContext hook example (open in anonymous browser #paywall) + */ +export const amplitudeContext = React.createContext(initialContext); + +export default amplitudeContext; diff --git a/src/stores/cypressContext.tsx b/src/stores/cypressContext.tsx new file mode 100644 index 000000000..921df6441 --- /dev/null +++ b/src/stores/cypressContext.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export type CypressContext = { + isCypressRunning?: boolean; +}; + +/** + * Uses native React Context API + * + * @example Usage + * import cypressContext from './src/stores/cypressContext'; + * const { locale, lang }: CustomerContext = React.useContext(cypressContext); + * + * @see https://reactjs.org/docs/context.html + * @see https://medium.com/better-programming/react-hooks-usecontext-30eb560999f for useContext hook example (open in anonymous browser #paywall) + */ +export const cypressContext = React.createContext({}); + +export default cypressContext; diff --git a/src/stores/datasetContext.tsx b/src/stores/datasetContext.tsx new file mode 100644 index 000000000..aa7f8ef16 --- /dev/null +++ b/src/stores/datasetContext.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { GraphCMSDataset } from '../utils/graphCMSDataset/GraphCMSDataset'; + +export type DatasetContext = GraphCMSDataset; + +/** + * Uses native React Context API + * + * @example Usage + * import datasetContext from './src/stores/datasetContext'; + * const dataset: DatasetContext = React.useContext(datasetContext); + * + * @see https://reactjs.org/docs/context.html + * @see https://medium.com/better-programming/react-hooks-usecontext-30eb560999f for useContext hook example (open in anonymous browser #paywall) + */ +export const datasetContext = React.createContext(null); + +export default datasetContext; diff --git a/src/stores/previewModeContext.tsx b/src/stores/previewModeContext.tsx index 2bea8d332..c608ba771 100644 --- a/src/stores/previewModeContext.tsx +++ b/src/stores/previewModeContext.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { PreviewData } from '../types/nextjs/PreviewData'; export type PreviewModeContext = { - preview: boolean; + isPreviewModeEnabled: boolean; previewData: PreviewData; } diff --git a/src/stores/quickPreviewContext.tsx b/src/stores/quickPreviewContext.tsx new file mode 100644 index 000000000..e25f65c78 --- /dev/null +++ b/src/stores/quickPreviewContext.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +export type QuickPreviewContext = { + isQuickPreviewPage: boolean; +} + +/** + * Uses native React Context API + * + * @example Usage + * import quickPreviewContext from './src/stores/quickPreviewContext'; + * const { isQuickPreviewPage }: quickPreviewContext = React.useContext(quickPreviewContext); + * + * @see https://reactjs.org/docs/context.html + * @see https://medium.com/better-programming/react-hooks-usecontext-30eb560999f for useContext hook example (open in anonymous browser #paywall) + */ +export const quickPreviewContext = React.createContext(null); + +export default quickPreviewContext; diff --git a/src/stores/userConsentContext.tsx b/src/stores/userConsentContext.tsx index 891b0af7b..db83bf729 100644 --- a/src/stores/userConsentContext.tsx +++ b/src/stores/userConsentContext.tsx @@ -3,6 +3,11 @@ import { UserConsent } from '../types/UserConsent'; export type UserConsentContext = UserConsent +/** + * Initial context, used by default until the Context Provider is initialised. + * + * @default Empty object, to allow for destructuring even when the context hasn't been initialised (on the server) + */ const initialContext = {}; /** diff --git a/src/stores/userSessionContext.tsx b/src/stores/userSessionContext.tsx index 8f6be878e..35fe929d2 100644 --- a/src/stores/userSessionContext.tsx +++ b/src/stores/userSessionContext.tsx @@ -11,6 +11,11 @@ import { UserSemiPersistentSession } from '../types/UserSemiPersistentSession'; */ export type UserSessionContext = Partial +/** + * Initial context, used by default until the Context Provider is initialised. + * + * @default Empty object, to allow for destructuring even when the context hasn't been initialised (on the server) + */ const initialContext = {}; /** diff --git a/src/svg/AnimatedBubble.svg b/src/svg/AnimatedBubble.svg new file mode 100644 index 000000000..f960790f9 --- /dev/null +++ b/src/svg/AnimatedBubble.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/svg/AnimatedTextBubble.svg b/src/svg/AnimatedTextBubble.svg new file mode 100644 index 000000000..36c9c4b9d --- /dev/null +++ b/src/svg/AnimatedTextBubble.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/types/CSSStyles.ts b/src/types/CSSStyles.ts index 1d5eead0a..2bb178f88 100644 --- a/src/types/CSSStyles.ts +++ b/src/types/CSSStyles.ts @@ -1,6 +1,6 @@ -import * as CSS from 'csstype'; +import { GenericObject } from './GenericObject'; /** * Represents a CSS "styles" object. */ -export type CSSStyles = CSS.Properties +export type CSSStyles = GenericObject; diff --git a/src/types/DateDay.ts b/src/types/DateDay.ts new file mode 100644 index 000000000..47062c367 --- /dev/null +++ b/src/types/DateDay.ts @@ -0,0 +1,8 @@ +/** + * Represents a string containing a date value. + * + * @example 2020-10-25 + * + * There is no difference with the TS "number" type, but it helps make obvious what the real data type is. + */ +export type DateDay = string; diff --git a/src/types/Flatted.ts b/src/types/Flatted.ts new file mode 100644 index 000000000..30409d074 --- /dev/null +++ b/src/types/Flatted.ts @@ -0,0 +1,26 @@ +/** + * Fast and minimal circular JSON parser. + * logic example + ```js + var a = [{one: 1}, {two: '2'}]; + a[0].a = a; + // a is the main object, will be at index '0' + // {one: 1} is the second object, index '1' + // {two: '2'} the third, in '2', and it has a string + // which will be found at index '3' + Flatted.stringify(a); + // [["1","2"],{"one":1,"a":"0"},{"two":"3"},"2"] + // a[one,two] {one: 1, a} {two: '2'} '2' + ``` + */ + +/** + * We use the "Flatted" type when we Flatted.stringify a variable. + * This helps being explicit about the fact it should be Flatted.parse instead of JSON.parsed. + * + * @property ParsedType Useful for documentation purpose, doesn't do anything. + * Helps indicate what the type of the data will be, once parsed. + * + * XXX Flatted is supposed to provide its own TS typings, but I couldn't figure out how to use them. + */ +export type Flatted = string; diff --git a/src/types/Float.ts b/src/types/Float.ts new file mode 100644 index 000000000..4ef9876a3 --- /dev/null +++ b/src/types/Float.ts @@ -0,0 +1,6 @@ +/** + * Represents a number containing a float value. + * + * There is no difference with the TS "number" type, but it helps make obvious what the real data type is. + */ +export type Float = number; diff --git a/src/types/I18nMarkdown.ts b/src/types/I18nMarkdown.ts new file mode 100644 index 000000000..e5b07b9c1 --- /dev/null +++ b/src/types/I18nMarkdown.ts @@ -0,0 +1,7 @@ +import { Markdown } from './Markdown'; + +/** + * I18n markdown field auto computed by our own algorithm + * (we don't use the Airtable field, it is calculated again instead in "sanitizeRecord") + */ +export type I18nMarkdown = Markdown; diff --git a/src/types/I18nRichText.ts b/src/types/I18nRichText.ts new file mode 100644 index 000000000..11767ec82 --- /dev/null +++ b/src/types/I18nRichText.ts @@ -0,0 +1,6 @@ +import { RichText } from './RichText'; + +/** + * Same as RichText, but make it obvious it's a localized field. + */ +export type I18nRichText = RichText diff --git a/src/types/I18nString.ts b/src/types/I18nString.ts new file mode 100644 index 000000000..d367f3cf6 --- /dev/null +++ b/src/types/I18nString.ts @@ -0,0 +1,5 @@ +/** + * I18n field auto computed by our own algorithm + * (we don't use the Airtable field, it is calculated again instead in "sanitizeRecord") + */ +export type I18nString = string; diff --git a/src/types/Integer.ts b/src/types/Integer.ts new file mode 100644 index 000000000..95b4cf3d2 --- /dev/null +++ b/src/types/Integer.ts @@ -0,0 +1,6 @@ +/** + * Represents a number containing an integer value. + * + * There is no difference with the TS "number" type, but it helps make obvious what the real data type is. + */ +export type Integer = number; diff --git a/src/types/Markdown.ts b/src/types/Markdown.ts index 906edbfe1..03c9f54cd 100644 --- a/src/types/Markdown.ts +++ b/src/types/Markdown.ts @@ -1 +1,6 @@ +/** + * Represents a string containing Markdown. + * + * There is no difference with the TS "string" type, but it helps make obvious what the real data type is. + */ export type Markdown = string; diff --git a/src/types/ReactSelect.ts b/src/types/ReactSelect.ts new file mode 100644 index 000000000..12c184ed9 --- /dev/null +++ b/src/types/ReactSelect.ts @@ -0,0 +1,21 @@ +/** + * A react-select group + * + * @see https://react-select.com/advanced#replacing-builtins + * @see https://stackoverflow.com/a/52503863/2391795 + */ +export type ReactSelectGroup = { + label: string; + labelShort?: string; + options: Array; +} + +/** + * A react-select option must have a label and a value field + * + * But those two keys can be changed from within the component using getOptionLabel and getOptionValue + */ +export type ReactSelectDefaultOption = { + label: string; + value: string; +} diff --git a/src/types/data/AssetThumbnail.ts b/src/types/data/AssetThumbnail.ts new file mode 100644 index 000000000..8f58ee020 --- /dev/null +++ b/src/types/data/AssetThumbnail.ts @@ -0,0 +1,5 @@ +export type AssetThumbnail = { + url: string; + width: number; + height: number; +}; diff --git a/src/types/data/Customer.ts b/src/types/data/Customer.ts index 1077233ff..161901bf9 100644 --- a/src/types/data/Customer.ts +++ b/src/types/data/Customer.ts @@ -1,11 +1,16 @@ -import { RichText } from '../RichText'; +import { I18nRichText } from '../I18nRichText'; import { GraphCMSSystemFields } from './GraphCMSSystemFields'; +import { Product } from './Product'; import { Theme } from './Theme'; export type Customer = { id?: string; ref?: string; label?: string; + availableLanguages: string[]; + products?: Product[]; theme?: Theme; - terms?: RichText; + serviceLabel?: string; + termsDescription?: I18nRichText; + privacyDescription?: I18nRichText; } & GraphCMSSystemFields; diff --git a/src/types/data/CustomerTheme.ts b/src/types/data/CustomerTheme.ts new file mode 100644 index 000000000..05776b961 --- /dev/null +++ b/src/types/data/CustomerTheme.ts @@ -0,0 +1,13 @@ +import { Theme } from './Theme'; + +/** + * Simplified version of the Theme. + * + * CustomerTheme is what's really used for theming, and doesn't include useless properties from the Theme entity. + * Also, all properties defined in Theme will be set in CustomerTheme, because fallback values will have been applied. + * + * Inspired by MaterialUI Design system. + * + * @see https://material.io/design/color/the-color-system.html#color-theme-creation + */ +export type CustomerTheme = Required> & {}; diff --git a/src/types/data/Theme.ts b/src/types/data/Theme.ts index 9b19fd80a..551d2c27c 100644 --- a/src/types/data/Theme.ts +++ b/src/types/data/Theme.ts @@ -2,7 +2,18 @@ import { Asset } from './Asset'; import { GraphCMSSystemFields } from './GraphCMSSystemFields'; export type Theme = { - id?: string; primaryColor?: string; + primaryColorVariant1?: string; + onPrimaryColor?: string; + secondaryColor?: string; + secondaryColorVariant1?: string; + onSecondaryColor?: string; + backgroundColor?: string; + onBackgroundColor?: string; + surfaceColor?: string; + onSurfaceColor?: string; + errorColor?: string; + onErrorColor?: string; logo?: Asset; + fonts?: string; } & GraphCMSSystemFields; diff --git a/src/types/data/VisibilityStatus.ts b/src/types/data/VisibilityStatus.ts new file mode 100644 index 000000000..338d7a2ca --- /dev/null +++ b/src/types/data/VisibilityStatus.ts @@ -0,0 +1,8 @@ +/** + * Whether the record will be displayed or hidden to the end-users. + */ +export const VISIBILITY_STATUS_ARCHIVED = 'Archivé'; +export const VISIBILITY_STATUS_DRAFT = 'Brouillon'; +export const VISIBILITY_STATUS_PUBLISHED = 'Publié'; + +export type VisibilityStatus = typeof VISIBILITY_STATUS_ARCHIVED | typeof VISIBILITY_STATUS_DRAFT | typeof VISIBILITY_STATUS_PUBLISHED; diff --git a/src/types/nextjs/MultiversalAppBootstrapPageProps.ts b/src/types/nextjs/MultiversalAppBootstrapPageProps.ts index f6f374735..4e955c571 100644 --- a/src/types/nextjs/MultiversalAppBootstrapPageProps.ts +++ b/src/types/nextjs/MultiversalAppBootstrapPageProps.ts @@ -1,5 +1,5 @@ import { i18n } from 'i18next'; -import { Theme } from '../data/Theme'; +import { CustomerTheme } from '../data/CustomerTheme'; /** * Additional props that are injected by MultiversalAppBootstrap to all pages @@ -7,5 +7,5 @@ import { Theme } from '../data/Theme'; export type MultiversalAppBootstrapPageProps = { i18nextInstance: i18n; isSSGFallbackInitialBuild: boolean; // When true, means the app is loading a SSG page, with fallback mode enabled, and this page hasn't been built before - theme: Theme; + customerTheme: CustomerTheme; } diff --git a/src/types/pageProps/MultiversalPageProps.ts b/src/types/pageProps/MultiversalPageProps.ts index 61e721c03..afce79365 100644 --- a/src/types/pageProps/MultiversalPageProps.ts +++ b/src/types/pageProps/MultiversalPageProps.ts @@ -1,6 +1,5 @@ import { NormalizedCacheObject } from 'apollo-cache-inmemory'; import { I18nextResources } from '../../utils/i18n/i18nextLocize'; -import { Customer } from '../data/Customer'; /** * Page properties available on all pages, whether they're rendered statically, dynamically, from the server or the client @@ -12,7 +11,7 @@ import { Customer } from '../data/Customer'; export type MultiversalPageProps = { apolloState: NormalizedCacheObject; bestCountryCodes: string[]; - customer: Customer; + serializedDataset: string; // Transferred from server to browser as JSON (using Flatten.stringify), then parsed on the browser/server within the MultiversalAppBootstrap customerRef: string; error?: Error; // Only defined if there was an error gcmsLocales: string; diff --git a/src/types/pageProps/SSRPageProps.ts b/src/types/pageProps/SSRPageProps.ts index acc028575..d97e5c517 100644 --- a/src/types/pageProps/SSRPageProps.ts +++ b/src/types/pageProps/SSRPageProps.ts @@ -13,6 +13,7 @@ import { OnlyServerPageProps } from './OnlyServerPageProps'; export type SSRPageProps = { // Props that are specific to SSR isServerRendering: boolean; + isQuickPreviewPage: boolean; } & MultiversalPageProps // Generic props that are provided immediately, no matter what & Partial // Pages served by SSR eventually benefit from props injected by the MultiversalAppBootstrap component & E; diff --git a/src/types/react/ReactButtonProps.ts b/src/types/react/ReactButtonProps.ts new file mode 100644 index 000000000..e2388fe2c --- /dev/null +++ b/src/types/react/ReactButtonProps.ts @@ -0,0 +1,14 @@ +import { + ButtonHTMLAttributes, + DetailedHTMLProps, +} from 'react'; + +/** + * React HTML "Button" element properties. + * Meant to be a helper when using custom buttons that should inherit native "