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