From 019b4375ffb619827afb05205fdc1d8a444da4ec Mon Sep 17 00:00:00 2001 From: hpierre74 Date: Sun, 10 Oct 2021 22:37:56 +0200 Subject: [PATCH] feat(exercises): add react lazy exercise --- apps/with-react-lazy-e2e/.eslintrc.json | 17 ++ apps/with-react-lazy-e2e/cypress.json | 12 ++ .../src/fixtures/example.json | 4 + .../src/integration/app.spec.ts | 13 ++ apps/with-react-lazy-e2e/src/plugins/index.js | 22 +++ .../with-react-lazy-e2e/src/support/app.po.ts | 1 + .../src/support/commands.ts | 33 ++++ apps/with-react-lazy-e2e/src/support/index.ts | 17 ++ apps/with-react-lazy-e2e/tsconfig.e2e.json | 10 ++ apps/with-react-lazy-e2e/tsconfig.json | 10 ++ apps/with-react-lazy/.babelrc | 11 ++ apps/with-react-lazy/.browserslistrc | 16 ++ apps/with-react-lazy/.eslintrc.json | 18 +++ apps/with-react-lazy/jest.config.js | 10 ++ apps/with-react-lazy/src/app/app.spec.tsx | 21 +++ apps/with-react-lazy/src/app/app.tsx | 16 ++ .../app/components/gridTextField.component.js | 37 +++++ .../src/app/components/layout.component.js | 29 ++++ .../src/app/components/navbar.component.js | 142 ++++++++++++++++ .../src/app/constants/proptypes.constants.js | 7 + .../src/app/hooks/useInput.hook.js | 9 ++ .../src/app/hooks/useOnLeave.hook.js | 19 +++ .../src/app/hooks/useStepperForm.hook.js | 13 ++ .../src/app/hooks/useStepperFormChild.hook.js | 22 +++ apps/with-react-lazy/src/app/logo.svg | 1 + .../__tests__/articles.actions.spec.js | 20 +++ .../__tests__/articles.context.spec.js | 25 +++ .../__tests__/articles.reducer.spec.js | 36 +++++ .../app/modules/articles/articles.actions.js | 9 ++ .../app/modules/articles/articles.context.js | 52 ++++++ .../app/modules/articles/articles.reducer.js | 16 ++ .../modules/articles/articles.selectors.js | 10 ++ .../articles/components/article.component.js | 16 ++ .../components/articleCard.component.js | 90 +++++++++++ .../components/articlesList.component.js | 19 +++ .../cart/__tests__/cart.actions.spec.js | 27 ++++ .../cart/__tests__/cart.context.spec.js | 21 +++ .../cart/__tests__/cart.reducer.spec.js | 107 ++++++++++++ .../src/app/modules/cart/cart.actions.js | 13 ++ .../src/app/modules/cart/cart.context.js | 74 +++++++++ .../src/app/modules/cart/cart.reducer.js | 79 +++++++++ .../modules/cart/components/cart.component.js | 110 +++++++++++++ .../cart/components/cartLayout.component.js | 22 +++ .../modules/checkout/checkout.component.js | 153 ++++++++++++++++++ .../modules/checkout/checkout.constants.js | 5 + .../components/addressForm.component.js | 78 +++++++++ .../components/paymentForm.component.js | 68 ++++++++ .../checkout/components/review.component.js | 118 ++++++++++++++ .../routing/__tests__/rooting.hooks.spec.js | 142 ++++++++++++++++ .../routing/components/routes.component.js | 50 ++++++ .../app/modules/routing/routing.constants.js | 10 ++ .../src/app/modules/routing/routing.hooks.js | 35 ++++ .../user/__tests__/user.actions.spec.js | 45 ++++++ .../user/__tests__/user.context.spec.js | 23 +++ .../user/__tests__/user.reducer.spec.js | 29 ++++ .../user/__tests__/user.selectors.spec.js | 19 +++ .../user/components/login.component.js | 123 ++++++++++++++ .../src/app/modules/user/user.actions.js | 35 ++++ .../src/app/modules/user/user.context.js | 54 +++++++ .../src/app/modules/user/user.hooks.js | 5 + .../src/app/modules/user/user.reducer.js | 25 +++ .../src/app/modules/user/user.selectors.js | 2 + .../src/app/pages/about.page.js | 26 +++ .../src/app/pages/article.page.js | 27 ++++ .../src/app/pages/checkout.page.js | 12 ++ .../src/app/pages/contact.page.js | 26 +++ .../src/app/pages/home.page.js | 16 ++ .../src/app/pages/login.page.js | 12 ++ .../src/app/utils/context.utils.js | 33 ++++ apps/with-react-lazy/src/assets/.gitkeep | 0 apps/with-react-lazy/src/assets/README-fr.md | 13 ++ apps/with-react-lazy/src/assets/README.md | 11 ++ apps/with-react-lazy/src/assets/articles.json | 72 +++++++++ .../src/environments/environment.prod.ts | 3 + .../src/environments/environment.ts | 6 + apps/with-react-lazy/src/favicon.ico | Bin 0 -> 15086 bytes apps/with-react-lazy/src/index.html | 14 ++ apps/with-react-lazy/src/main.tsx | 19 +++ apps/with-react-lazy/src/polyfills.ts | 7 + apps/with-react-lazy/src/styles.css | 14 ++ apps/with-react-lazy/tsconfig.app.json | 13 ++ apps/with-react-lazy/tsconfig.json | 23 +++ apps/with-react-lazy/tsconfig.spec.json | 19 +++ nx.json | 7 + workspace.json | 102 ++++++++++++ 85 files changed, 2750 insertions(+) create mode 100644 apps/with-react-lazy-e2e/.eslintrc.json create mode 100644 apps/with-react-lazy-e2e/cypress.json create mode 100644 apps/with-react-lazy-e2e/src/fixtures/example.json create mode 100644 apps/with-react-lazy-e2e/src/integration/app.spec.ts create mode 100644 apps/with-react-lazy-e2e/src/plugins/index.js create mode 100644 apps/with-react-lazy-e2e/src/support/app.po.ts create mode 100644 apps/with-react-lazy-e2e/src/support/commands.ts create mode 100644 apps/with-react-lazy-e2e/src/support/index.ts create mode 100644 apps/with-react-lazy-e2e/tsconfig.e2e.json create mode 100644 apps/with-react-lazy-e2e/tsconfig.json create mode 100644 apps/with-react-lazy/.babelrc create mode 100644 apps/with-react-lazy/.browserslistrc create mode 100644 apps/with-react-lazy/.eslintrc.json create mode 100644 apps/with-react-lazy/jest.config.js create mode 100644 apps/with-react-lazy/src/app/app.spec.tsx create mode 100644 apps/with-react-lazy/src/app/app.tsx create mode 100644 apps/with-react-lazy/src/app/components/gridTextField.component.js create mode 100644 apps/with-react-lazy/src/app/components/layout.component.js create mode 100644 apps/with-react-lazy/src/app/components/navbar.component.js create mode 100644 apps/with-react-lazy/src/app/constants/proptypes.constants.js create mode 100644 apps/with-react-lazy/src/app/hooks/useInput.hook.js create mode 100644 apps/with-react-lazy/src/app/hooks/useOnLeave.hook.js create mode 100644 apps/with-react-lazy/src/app/hooks/useStepperForm.hook.js create mode 100644 apps/with-react-lazy/src/app/hooks/useStepperFormChild.hook.js create mode 100644 apps/with-react-lazy/src/app/logo.svg create mode 100644 apps/with-react-lazy/src/app/modules/articles/__tests__/articles.actions.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/__tests__/articles.context.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/__tests__/articles.reducer.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/articles.actions.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/articles.context.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/articles.reducer.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/articles.selectors.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/components/article.component.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/components/articleCard.component.js create mode 100644 apps/with-react-lazy/src/app/modules/articles/components/articlesList.component.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/__tests__/cart.actions.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/__tests__/cart.context.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/__tests__/cart.reducer.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/cart.actions.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/cart.context.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/cart.reducer.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/components/cart.component.js create mode 100644 apps/with-react-lazy/src/app/modules/cart/components/cartLayout.component.js create mode 100644 apps/with-react-lazy/src/app/modules/checkout/checkout.component.js create mode 100644 apps/with-react-lazy/src/app/modules/checkout/checkout.constants.js create mode 100644 apps/with-react-lazy/src/app/modules/checkout/components/addressForm.component.js create mode 100644 apps/with-react-lazy/src/app/modules/checkout/components/paymentForm.component.js create mode 100644 apps/with-react-lazy/src/app/modules/checkout/components/review.component.js create mode 100644 apps/with-react-lazy/src/app/modules/routing/__tests__/rooting.hooks.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/routing/components/routes.component.js create mode 100644 apps/with-react-lazy/src/app/modules/routing/routing.constants.js create mode 100644 apps/with-react-lazy/src/app/modules/routing/routing.hooks.js create mode 100644 apps/with-react-lazy/src/app/modules/user/__tests__/user.actions.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/user/__tests__/user.context.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/user/__tests__/user.reducer.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/user/__tests__/user.selectors.spec.js create mode 100644 apps/with-react-lazy/src/app/modules/user/components/login.component.js create mode 100644 apps/with-react-lazy/src/app/modules/user/user.actions.js create mode 100644 apps/with-react-lazy/src/app/modules/user/user.context.js create mode 100644 apps/with-react-lazy/src/app/modules/user/user.hooks.js create mode 100644 apps/with-react-lazy/src/app/modules/user/user.reducer.js create mode 100644 apps/with-react-lazy/src/app/modules/user/user.selectors.js create mode 100644 apps/with-react-lazy/src/app/pages/about.page.js create mode 100644 apps/with-react-lazy/src/app/pages/article.page.js create mode 100644 apps/with-react-lazy/src/app/pages/checkout.page.js create mode 100644 apps/with-react-lazy/src/app/pages/contact.page.js create mode 100644 apps/with-react-lazy/src/app/pages/home.page.js create mode 100644 apps/with-react-lazy/src/app/pages/login.page.js create mode 100644 apps/with-react-lazy/src/app/utils/context.utils.js create mode 100644 apps/with-react-lazy/src/assets/.gitkeep create mode 100644 apps/with-react-lazy/src/assets/README-fr.md create mode 100644 apps/with-react-lazy/src/assets/README.md create mode 100644 apps/with-react-lazy/src/assets/articles.json create mode 100644 apps/with-react-lazy/src/environments/environment.prod.ts create mode 100644 apps/with-react-lazy/src/environments/environment.ts create mode 100644 apps/with-react-lazy/src/favicon.ico create mode 100644 apps/with-react-lazy/src/index.html create mode 100644 apps/with-react-lazy/src/main.tsx create mode 100644 apps/with-react-lazy/src/polyfills.ts create mode 100644 apps/with-react-lazy/src/styles.css create mode 100644 apps/with-react-lazy/tsconfig.app.json create mode 100644 apps/with-react-lazy/tsconfig.json create mode 100644 apps/with-react-lazy/tsconfig.spec.json diff --git a/apps/with-react-lazy-e2e/.eslintrc.json b/apps/with-react-lazy-e2e/.eslintrc.json new file mode 100644 index 0000000..4c5989b --- /dev/null +++ b/apps/with-react-lazy-e2e/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["src/plugins/index.js"], + "rules": { + "@typescript-eslint/no-var-requires": "off", + "no-undef": "off" + } + } + ] +} diff --git a/apps/with-react-lazy-e2e/cypress.json b/apps/with-react-lazy-e2e/cypress.json new file mode 100644 index 0000000..a7caa6e --- /dev/null +++ b/apps/with-react-lazy-e2e/cypress.json @@ -0,0 +1,12 @@ +{ + "fileServerFolder": ".", + "fixturesFolder": "./src/fixtures", + "integrationFolder": "./src/integration", + "modifyObstructiveCode": false, + "pluginsFile": "./src/plugins/index", + "supportFile": "./src/support/index.ts", + "video": true, + "videosFolder": "../../dist/cypress/apps/with-react-lazy-e2e/videos", + "screenshotsFolder": "../../dist/cypress/apps/with-react-lazy-e2e/screenshots", + "chromeWebSecurity": false +} diff --git a/apps/with-react-lazy-e2e/src/fixtures/example.json b/apps/with-react-lazy-e2e/src/fixtures/example.json new file mode 100644 index 0000000..294cbed --- /dev/null +++ b/apps/with-react-lazy-e2e/src/fixtures/example.json @@ -0,0 +1,4 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io" +} diff --git a/apps/with-react-lazy-e2e/src/integration/app.spec.ts b/apps/with-react-lazy-e2e/src/integration/app.spec.ts new file mode 100644 index 0000000..b28b839 --- /dev/null +++ b/apps/with-react-lazy-e2e/src/integration/app.spec.ts @@ -0,0 +1,13 @@ +import { getGreeting } from '../support/app.po'; + +describe('with-react-lazy', () => { + beforeEach(() => cy.visit('/')); + + it('should display welcome message', () => { + // Custom command example, see `../support/commands.ts` file + cy.login('my-email@something.com', 'myPassword'); + + // Function helper example, see `../support/app.po.ts` file + getGreeting().contains('Welcome to with-react-lazy!'); + }); +}); diff --git a/apps/with-react-lazy-e2e/src/plugins/index.js b/apps/with-react-lazy-e2e/src/plugins/index.js new file mode 100644 index 0000000..9067e75 --- /dev/null +++ b/apps/with-react-lazy-e2e/src/plugins/index.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + + // Preprocess Typescript file using Nx helper + on('file:preprocessor', preprocessTypescript(config)); +}; diff --git a/apps/with-react-lazy-e2e/src/support/app.po.ts b/apps/with-react-lazy-e2e/src/support/app.po.ts new file mode 100644 index 0000000..3293424 --- /dev/null +++ b/apps/with-react-lazy-e2e/src/support/app.po.ts @@ -0,0 +1 @@ +export const getGreeting = () => cy.get('h1'); diff --git a/apps/with-react-lazy-e2e/src/support/commands.ts b/apps/with-react-lazy-e2e/src/support/commands.ts new file mode 100644 index 0000000..310f1fa --- /dev/null +++ b/apps/with-react-lazy-e2e/src/support/commands.ts @@ -0,0 +1,33 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + login(email: string, password: string): void; + } +} +// +// -- This is a parent command -- +Cypress.Commands.add('login', (email, password) => { + console.log('Custom command example: 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) => { ... }) diff --git a/apps/with-react-lazy-e2e/src/support/index.ts b/apps/with-react-lazy-e2e/src/support/index.ts new file mode 100644 index 0000000..3d469a6 --- /dev/null +++ b/apps/with-react-lazy-e2e/src/support/index.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; diff --git a/apps/with-react-lazy-e2e/tsconfig.e2e.json b/apps/with-react-lazy-e2e/tsconfig.e2e.json new file mode 100644 index 0000000..9dc3660 --- /dev/null +++ b/apps/with-react-lazy-e2e/tsconfig.e2e.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false, + "outDir": "../../dist/out-tsc", + "allowJs": true, + "types": ["cypress", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.js"] +} diff --git a/apps/with-react-lazy-e2e/tsconfig.json b/apps/with-react-lazy-e2e/tsconfig.json new file mode 100644 index 0000000..08841a7 --- /dev/null +++ b/apps/with-react-lazy-e2e/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.e2e.json" + } + ] +} diff --git a/apps/with-react-lazy/.babelrc b/apps/with-react-lazy/.babelrc new file mode 100644 index 0000000..61641ec --- /dev/null +++ b/apps/with-react-lazy/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic" + } + ] + ], + "plugins": [] +} diff --git a/apps/with-react-lazy/.browserslistrc b/apps/with-react-lazy/.browserslistrc new file mode 100644 index 0000000..f1d12df --- /dev/null +++ b/apps/with-react-lazy/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by: +# 1. autoprefixer to adjust CSS to support the below specified browsers +# 2. babel preset-env to adjust included polyfills +# +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# If you need to support different browsers in production, you may tweak the list below. + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major version +last 2 iOS major versions +Firefox ESR +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/apps/with-react-lazy/.eslintrc.json b/apps/with-react-lazy/.eslintrc.json new file mode 100644 index 0000000..734ddac --- /dev/null +++ b/apps/with-react-lazy/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/with-react-lazy/jest.config.js b/apps/with-react-lazy/jest.config.js new file mode 100644 index 0000000..1fc6216 --- /dev/null +++ b/apps/with-react-lazy/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + displayName: 'with-react-lazy', + preset: '../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/apps/with-react-lazy', +}; diff --git a/apps/with-react-lazy/src/app/app.spec.tsx b/apps/with-react-lazy/src/app/app.spec.tsx new file mode 100644 index 0000000..ba16f4c --- /dev/null +++ b/apps/with-react-lazy/src/app/app.spec.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import mockArticles from '../assets/articles.json'; + +import App from './app'; + +jest.mock('@react-course-v2/api', () => ({ + getArticles: jest.fn().mockResolvedValue(mockArticles), +})); + +describe('App', () => { + it('should render successfully', async () => { + let wrapper = { baseElement: undefined }; + await act(async () => { + wrapper = render(); + }); + + const { baseElement } = wrapper; + expect(baseElement).toBeTruthy(); + }); +}); diff --git a/apps/with-react-lazy/src/app/app.tsx b/apps/with-react-lazy/src/app/app.tsx new file mode 100644 index 0000000..5abb118 --- /dev/null +++ b/apps/with-react-lazy/src/app/app.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { UserProvider } from './modules/user/user.context'; + +import { AppRoutes } from './modules/routing/components/routes.component'; + +export default function App() { + return ( + + + + + + ); +} diff --git a/apps/with-react-lazy/src/app/components/gridTextField.component.js b/apps/with-react-lazy/src/app/components/gridTextField.component.js new file mode 100644 index 0000000..aab6c41 --- /dev/null +++ b/apps/with-react-lazy/src/app/components/gridTextField.component.js @@ -0,0 +1,37 @@ +import React, { memo } from 'react'; +import PropTypes from 'prop-types'; +import Grid from '@material-ui/core/Grid'; +import TextField from '@material-ui/core/TextField'; +import { useInput } from '../hooks/useInput.hook'; + +export const GridTextField = memo( + ({ initialState, props, gridProps, inputName, onBlur }) => { + const [value, onChange] = useInput(initialState); + + return ( + + + + ); + }, +); + +GridTextField.displayName = 'GridTextField'; + +GridTextField.propTypes = { + props: PropTypes.object.isRequired, + gridProps: PropTypes.object.isRequired, + inputName: PropTypes.string.isRequired, + onBlur: PropTypes.func.isRequired, + initialState: PropTypes.any, +}; diff --git a/apps/with-react-lazy/src/app/components/layout.component.js b/apps/with-react-lazy/src/app/components/layout.component.js new file mode 100644 index 0000000..d8bede0 --- /dev/null +++ b/apps/with-react-lazy/src/app/components/layout.component.js @@ -0,0 +1,29 @@ +import React from 'react'; + +import Container from '@material-ui/core/Container'; +import { makeStyles } from '@material-ui/styles'; + +import NavBar from './navbar.component'; + +import { CHILDREN_PROP_TYPES } from '../constants/proptypes.constants'; + +const useStyles = makeStyles({ + container: { + marginTop: '2em', + }, +}); + +export const Layout = ({ children }) => { + const classes = useStyles(); + + return ( + <> + + {children} + + ); +}; + +Layout.propTypes = { + children: CHILDREN_PROP_TYPES, +}; diff --git a/apps/with-react-lazy/src/app/components/navbar.component.js b/apps/with-react-lazy/src/app/components/navbar.component.js new file mode 100644 index 0000000..629c19c --- /dev/null +++ b/apps/with-react-lazy/src/app/components/navbar.component.js @@ -0,0 +1,142 @@ +import React from 'react'; +import { Link, useHistory } from 'react-router-dom'; +import classnames from 'classnames'; + +import { makeStyles } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import { PowerSettingsNewOutlined } from '@material-ui/icons'; + +import { ROUTES_PATHS_BY_NAMES } from '../modules/routing/routing.constants'; +import { useUser } from '../modules/user/user.context'; +import { isUserConnected } from '../modules/user/user.selectors'; +import { logout } from '../modules/user/user.actions'; + +const useStyles = makeStyles(theme => ({ + root: { + flexGrow: 1, + }, + menuButton: { + transition: 'all 0.5s', + marginRight: theme.spacing(2), + }, + loginButton: { + color: theme.palette.success.main, + '&:hover': { + background: theme.palette.error.main, + color: 'white', + }, + }, + logoutButton: { + color: theme.palette.error.main, + '&:hover': { + background: theme.palette.success.main, + color: 'white', + }, + }, + title: { + flexGrow: 1, + }, +})); + +export default function NavBar() { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const [userState, dispatch] = useUser(); + const isConnected = isUserConnected(userState); + const { push } = useHistory(); + + const handleMenu = event => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const logInAndOut = () => { + isConnected ? dispatch(logout()) : push(ROUTES_PATHS_BY_NAMES.login); + }; + + return ( + + + + Shopping App + + + + +
+ + + + + + Home + + + Contact + + + About + + +
+
+
+ ); +} diff --git a/apps/with-react-lazy/src/app/constants/proptypes.constants.js b/apps/with-react-lazy/src/app/constants/proptypes.constants.js new file mode 100644 index 0000000..bfe9d9a --- /dev/null +++ b/apps/with-react-lazy/src/app/constants/proptypes.constants.js @@ -0,0 +1,7 @@ +import PropTypes from 'prop-types'; + +export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ + PropTypes.array.isRequired, + PropTypes.object, + PropTypes.element, +]).isRequired; diff --git a/apps/with-react-lazy/src/app/hooks/useInput.hook.js b/apps/with-react-lazy/src/app/hooks/useInput.hook.js new file mode 100644 index 0000000..a6942d5 --- /dev/null +++ b/apps/with-react-lazy/src/app/hooks/useInput.hook.js @@ -0,0 +1,9 @@ +import { useState } from 'react'; + +export const useInput = (initialValue = '') => { + const [inputValue, setInputValue] = useState(initialValue); + + const handleChange = e => setInputValue(e.target.value); + + return [inputValue, handleChange]; +}; diff --git a/apps/with-react-lazy/src/app/hooks/useOnLeave.hook.js b/apps/with-react-lazy/src/app/hooks/useOnLeave.hook.js new file mode 100644 index 0000000..9357e17 --- /dev/null +++ b/apps/with-react-lazy/src/app/hooks/useOnLeave.hook.js @@ -0,0 +1,19 @@ +/* eslint-disable no-unused-vars */ +import { useCallback, useState } from 'react'; + +export const useOnLeaveInput = initialState => { + const [formState, setFormState] = useState(initialState); + + const onBlur = useCallback(event => { + const targetName = event.target.name; + const targetValue = event.target.value; + + setFormState(prevState => + prevState[targetName] === targetValue + ? prevState + : { ...prevState, [targetName]: targetValue }, + ); + }, []); + + return [formState, onBlur]; +}; diff --git a/apps/with-react-lazy/src/app/hooks/useStepperForm.hook.js b/apps/with-react-lazy/src/app/hooks/useStepperForm.hook.js new file mode 100644 index 0000000..5ffeafa --- /dev/null +++ b/apps/with-react-lazy/src/app/hooks/useStepperForm.hook.js @@ -0,0 +1,13 @@ +import { useState, useCallback } from 'react'; + +export const useStepperForm = initialState => { + const [formState, setFormState] = useState(initialState); + + const handleNext = useCallback((key, data) => { + setFormState(prevState => + prevState[key] === data ? prevState : { ...prevState, [key]: data }, + ); + }, []); + + return [formState, handleNext]; +}; diff --git a/apps/with-react-lazy/src/app/hooks/useStepperFormChild.hook.js b/apps/with-react-lazy/src/app/hooks/useStepperFormChild.hook.js new file mode 100644 index 0000000..7c713b5 --- /dev/null +++ b/apps/with-react-lazy/src/app/hooks/useStepperFormChild.hook.js @@ -0,0 +1,22 @@ +import { useRef, useEffect } from 'react'; +import { useOnLeaveInput } from './useOnLeave.hook'; + +export const useStepperFormChild = ({ initialState, setParentState, step }) => { + const [formState, onBlur] = useOnLeaveInput(initialState); + const stateRef = useRef(formState); + + useEffect(() => { + // keep state ref up to date + stateRef.current = formState; + }, [formState]); + + useEffect(() => { + return () => { + // use stateRef instead of formState to avoid cleaning on formState changes + // this way it only cleans on unmount + setParentState(step, stateRef.current); + }; + }, [step, setParentState]); + + return onBlur; +}; diff --git a/apps/with-react-lazy/src/app/logo.svg b/apps/with-react-lazy/src/app/logo.svg new file mode 100644 index 0000000..2e5df0d --- /dev/null +++ b/apps/with-react-lazy/src/app/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.actions.spec.js b/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.actions.spec.js new file mode 100644 index 0000000..9120274 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.actions.spec.js @@ -0,0 +1,20 @@ +import { RECEIVED_ARTICLES, requestArticles } from '../articles.actions'; + +jest.mock('@react-course-v2/api', () => ({ + getArticles: jest.fn().mockResolvedValue('foo'), +})); + +describe('articles.actions', () => { + let dispatch; + beforeEach(() => { + dispatch = jest.fn(); + }); + + it('should dispatch getArticles result', async () => { + await requestArticles()(dispatch); + expect(dispatch).toBeCalledWith({ + type: RECEIVED_ARTICLES, + articles: 'foo', + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.context.spec.js b/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.context.spec.js new file mode 100644 index 0000000..7317c6d --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.context.spec.js @@ -0,0 +1,25 @@ +import { + useArticles, + useArticlesState, + useArticlesDispatch, +} from '../articles.context'; + +describe('articles.context', () => { + describe('useArticlesDispatch', () => { + it('should be defined', () => { + expect(typeof useArticlesDispatch).toBe('function'); + }); + }); + + describe('useArticlesState', () => { + it('should be defined', () => { + expect(typeof useArticlesState).toBe('function'); + }); + }); + + describe('useArticles', () => { + it('should be defined', () => { + expect(typeof useArticles).toBe('function'); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.reducer.spec.js b/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.reducer.spec.js new file mode 100644 index 0000000..4a9dfc1 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/__tests__/articles.reducer.spec.js @@ -0,0 +1,36 @@ +import { RECEIVED_ARTICLES } from '../articles.actions'; +import { articlesReducer, initialState } from '../articles.reducer'; + +describe('articles.reducer', () => { + it('should set articles in the state', () => { + expect( + articlesReducer(initialState, { + type: RECEIVED_ARTICLES, + articles: [1, 2, 3], + }), + ).toMatchObject({ + ...initialState, + articles: [1, 2, 3], + }); + }); + + it('should spread the articles with state ones', () => { + const state = { + ...initialState, + articles: [1, 2, 3], + }; + + expect( + articlesReducer(state, { type: RECEIVED_ARTICLES, articles: [1, 2, 3] }), + ).toMatchObject({ + ...initialState, + articles: [1, 2, 3, 1, 2, 3], + }); + }); + + it('should throw when not passed articles iterable', () => { + expect(() => + articlesReducer(initialState, { type: RECEIVED_ARTICLES }), + ).toThrow(); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/articles/articles.actions.js b/apps/with-react-lazy/src/app/modules/articles/articles.actions.js new file mode 100644 index 0000000..f4cc0e3 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/articles.actions.js @@ -0,0 +1,9 @@ +import { getArticles } from '@react-course-v2/api'; + +export const RECEIVED_ARTICLES = 'articles/RECEIVED_ARTICLES'; + +export const requestArticles = () => async dispatch => { + const articles = await getArticles(); + + return dispatch({ type: RECEIVED_ARTICLES, articles }); +}; diff --git a/apps/with-react-lazy/src/app/modules/articles/articles.context.js b/apps/with-react-lazy/src/app/modules/articles/articles.context.js new file mode 100644 index 0000000..3f16199 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/articles.context.js @@ -0,0 +1,52 @@ +import React from 'react'; + +import { articlesReducer, initialState } from './articles.reducer'; + +import { dispatchThunk } from '../../utils/context.utils'; +import { CHILDREN_PROP_TYPES } from '../../constants/proptypes.constants'; + +const ArticlesStateContext = React.createContext(); +const ArticlesDispatchContext = React.createContext(); + +const ArticlesProvider = ({ children }) => { + const [state, dispatch] = React.useReducer(articlesReducer, initialState); + const getState = React.useCallback(() => state, [state]); + + return ( + + + {children} + + + ); +}; + +ArticlesProvider.propTypes = { + children: CHILDREN_PROP_TYPES, +}; + +function useArticlesState() { + const context = React.useContext(ArticlesStateContext); + if (context === undefined) { + throw new Error('useArticlesState must be used within a ArticlesProvider'); + } + return context; +} + +function useArticlesDispatch() { + const context = React.useContext(ArticlesDispatchContext); + if (context === undefined) { + throw new Error( + 'useArticlesDispatch must be used within a ArticlesProvider', + ); + } + return context; +} + +function useArticles() { + return [useArticlesState(), useArticlesDispatch()]; +} + +export { ArticlesProvider, useArticles, useArticlesState, useArticlesDispatch }; diff --git a/apps/with-react-lazy/src/app/modules/articles/articles.reducer.js b/apps/with-react-lazy/src/app/modules/articles/articles.reducer.js new file mode 100644 index 0000000..b6518e5 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/articles.reducer.js @@ -0,0 +1,16 @@ +import { RECEIVED_ARTICLES } from './articles.actions'; + +export const initialState = { + articles: [], +}; + +export const articlesReducer = (state, action) => { + switch (action.type) { + case RECEIVED_ARTICLES: { + return { ...state, articles: [...state.articles, ...action.articles] }; + } + default: { + throw new Error(`Unhandled action type: ${action.type}`); + } + } +}; diff --git a/apps/with-react-lazy/src/app/modules/articles/articles.selectors.js b/apps/with-react-lazy/src/app/modules/articles/articles.selectors.js new file mode 100644 index 0000000..a5ec396 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/articles.selectors.js @@ -0,0 +1,10 @@ +import { useArticles } from './articles.context'; +import { requestArticles } from './articles.actions'; +import { useSelector } from '../../utils/context.utils'; + +export const useArticlesSelector = () => + useSelector(useArticles, ({ articles }) => articles, { + shouldFetch: true, + fetchCondition: articles => articles.length === 0, + fetchAction: requestArticles, + }); diff --git a/apps/with-react-lazy/src/app/modules/articles/components/article.component.js b/apps/with-react-lazy/src/app/modules/articles/components/article.component.js new file mode 100644 index 0000000..4ac07c4 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/components/article.component.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { ArticleCard } from './articleCard.component'; +import { useArticlesSelector } from '../articles.selectors'; + +export const Article = ({ id }) => { + const articles = useArticlesSelector(); + const article = articles.find(item => item.slug === id); + + return article ? : null; +}; + +Article.propTypes = { + id: PropTypes.string.isRequired, +}; diff --git a/apps/with-react-lazy/src/app/modules/articles/components/articleCard.component.js b/apps/with-react-lazy/src/app/modules/articles/components/articleCard.component.js new file mode 100644 index 0000000..136d3d1 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/components/articleCard.component.js @@ -0,0 +1,90 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; + +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import CardMedia from '@material-ui/core/CardMedia'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; + +import { makeStyles } from '@material-ui/core/styles'; +import { addToCart } from '../../cart/cart.actions'; +import { useCart } from '../../cart/cart.context'; + +const useStyles = makeStyles({ + card: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + cardMedia: { + paddingTop: '56.25%', // 16:9 + }, + cardContent: { + flexGrow: 1, + }, + cardDescription: { + display: 'flex', + justifyContent: 'space-between', + }, +}); + +export function ArticleCard({ article }) { + const { name, year, image, slug, price } = article; + const classes = useStyles(); + const [, dispatch] = useCart(); + + const dispatchAddToCart = () => dispatch(addToCart(article)); + + return ( + + + + + + {name} + +
+ {year} + {price} $ +
+
+ + + + +
+
+ ); +} + +ArticleCard.propTypes = { + article: PropTypes.shape({ + name: PropTypes.string.isRequired, + year: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + image: PropTypes.string.isRequired, + slug: PropTypes.string.isRequired, + price: PropTypes.number.isRequired, + }).isRequired, +}; diff --git a/apps/with-react-lazy/src/app/modules/articles/components/articlesList.component.js b/apps/with-react-lazy/src/app/modules/articles/components/articlesList.component.js new file mode 100644 index 0000000..fa8e8a5 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/articles/components/articlesList.component.js @@ -0,0 +1,19 @@ +import React from 'react'; + +import Grid from '@material-ui/core/Grid'; + +import { ArticleCard } from './articleCard.component'; + +import { useArticlesSelector } from '../articles.selectors'; + +export function ArticlesList() { + const articles = useArticlesSelector(); + + return ( + + {articles.map(article => ( + + ))} + + ); +} diff --git a/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.actions.spec.js b/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.actions.spec.js new file mode 100644 index 0000000..56b470f --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.actions.spec.js @@ -0,0 +1,27 @@ +import { + addToCart, + removeFromCart, + ADD_TO_CART, + REMOVE_FROM_CART, +} from '../cart.actions'; + +describe('cart.actions', () => { + let dispatch; + beforeEach(() => { + dispatch = jest.fn(); + }); + + it('should dispatch getArticles result', async () => { + const article = { id: 'foo' }; + dispatch(addToCart(article)); + return expect(dispatch).toBeCalledWith({ type: ADD_TO_CART, article }); + }); + + it('should dispatch getArticles result', async () => { + dispatch(removeFromCart('foo')); + return expect(dispatch).toBeCalledWith({ + type: REMOVE_FROM_CART, + id: 'foo', + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.context.spec.js b/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.context.spec.js new file mode 100644 index 0000000..35c22f3 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.context.spec.js @@ -0,0 +1,21 @@ +import { useCart, useCartState, useCartDispatch } from '../cart.context'; + +describe('cart.context', () => { + describe('useCartDispatch', () => { + it('should be defined', () => { + expect(typeof useCartDispatch).toBe('function'); + }); + }); + + describe('useCartState', () => { + it('should be defined', () => { + expect(typeof useCartState).toBe('function'); + }); + }); + + describe('useCart', () => { + it('should be defined', () => { + expect(typeof useCart).toBe('function'); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.reducer.spec.js b/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.reducer.spec.js new file mode 100644 index 0000000..95726f4 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/__tests__/cart.reducer.spec.js @@ -0,0 +1,107 @@ +import { ADD_TO_CART, REMOVE_FROM_CART } from '../cart.actions'; +import { cartReducer, initialState } from '../cart.reducer'; + +describe('cart.reducer', () => { + describe('ADD_TO_CART', () => { + it('should set cart in the state', () => { + expect( + cartReducer(initialState, { + type: ADD_TO_CART, + article: { id: 'foo', price: 5 }, + }), + ).toMatchObject({ + ...initialState, + articles: { foo: { id: 'foo', price: 5 } }, + total: 5, + }); + }); + + it('should add a "occurrences" property of value 2 to an existing single article matching the action.article', () => { + const state = { + ...initialState, + articles: { foo: { id: 'foo', price: 5 } }, + total: 5, + }; + + expect( + cartReducer(state, { + type: ADD_TO_CART, + article: { id: 'foo', price: 5 }, + }), + ).toMatchObject({ + ...state, + articles: { foo: { id: 'foo', price: 5, occurrences: 2 } }, + total: 10, + }); + }); + + it('should increment the "occurrences" property when it is already set on a matching stored article', () => { + const state = { + ...initialState, + articles: { foo: { id: 'foo', occurrences: 2 } }, + total: 10, + }; + + expect( + cartReducer(state, { + type: ADD_TO_CART, + article: { id: 'foo', price: 5 }, + }), + ).toMatchObject({ + ...state, + articles: { foo: { id: 'foo', price: 5, occurrences: 3 } }, + total: 15, + }); + }); + }); + + describe('REMOVE_FROM_CART', () => { + it('should decrement the "occurrences" property when it is already set on a matching stored article', () => { + const state = { + ...initialState, + articles: { foo: { id: 'foo', price: 5, occurrences: 2 } }, + total: 10, + }; + + expect( + cartReducer(state, { type: REMOVE_FROM_CART, id: 'foo' }), + ).toMatchObject({ + ...state, + articles: { foo: { id: 'foo', price: 5, occurrences: 1 } }, + total: 5, + }); + }); + + it('should remove the matching stored article when its occurrences property is of value 1', () => { + const state = { + ...initialState, + articles: { foo: { id: 'foo', price: 5, occurrences: 1 } }, + total: 5, + }; + + expect( + cartReducer(state, { type: REMOVE_FROM_CART, id: 'foo' }), + ).toMatchObject({ + ...state, + articles: {}, + total: 0, + }); + }); + + it('should remove the matching stored article when it has no occurrences property set', () => { + const state = { + ...initialState, + articles: { foo: { id: 'foo', price: 5 } }, + total: 5, + }; + + expect( + cartReducer(state, { type: REMOVE_FROM_CART, id: 'foo' }), + ).toMatchObject({ + ...state, + articles: {}, + total: 0, + }); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/cart/cart.actions.js b/apps/with-react-lazy/src/app/modules/cart/cart.actions.js new file mode 100644 index 0000000..1fe6152 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/cart.actions.js @@ -0,0 +1,13 @@ +export const RESTORE_CART = 'cart/RESTORE_CART'; +export const ADD_TO_CART = 'cart/ADD_TO_CART'; +export const REMOVE_FROM_CART = 'cart/REMOVE_FROM_CART'; + +export const restoreCart = ({ articles, total }) => ({ + type: RESTORE_CART, + articles, + total, +}); + +export const addToCart = article => ({ type: ADD_TO_CART, article }); + +export const removeFromCart = id => ({ type: REMOVE_FROM_CART, id }); diff --git a/apps/with-react-lazy/src/app/modules/cart/cart.context.js b/apps/with-react-lazy/src/app/modules/cart/cart.context.js new file mode 100644 index 0000000..26b7712 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/cart.context.js @@ -0,0 +1,74 @@ +import React, { useEffect, useRef } from 'react'; + +import { cartReducer, initialState } from './cart.reducer'; + +import { dispatchThunk } from '../../utils/context.utils'; +import { CHILDREN_PROP_TYPES } from '../../constants/proptypes.constants'; +import { restoreCart } from './cart.actions'; + +const CartStateContext = React.createContext(); +const CartDispatchContext = React.createContext(); + +const persistCart = cart => () => { + localStorage.setItem('cart', JSON.stringify(cart)); +}; + +const CartProvider = ({ children }) => { + const isInitialized = useRef(false); + const [state, dispatch] = React.useReducer(cartReducer, initialState); + const getState = React.useCallback(() => state, [state]); + + useEffect(() => { + if (isInitialized && isInitialized.current) { + return; + } + + isInitialized.current = true; + const storedArticles = localStorage.getItem('cart'); + + if (storedArticles) dispatch(restoreCart(JSON.parse(storedArticles))); + }); + + useEffect(() => { + const callback = persistCart(state); + window.addEventListener('beforeunload', callback); + + return () => { + window.removeEventListener('beforeunload', callback); + }; + }, [state]); + + return ( + + + {children} + + + ); +}; + +CartProvider.propTypes = { + children: CHILDREN_PROP_TYPES, +}; + +function useCartState() { + const context = React.useContext(CartStateContext); + if (context === undefined) { + throw new Error('useCartState must be used within a CartProvider'); + } + return context; +} + +function useCartDispatch() { + const context = React.useContext(CartDispatchContext); + if (context === undefined) { + throw new Error('useCartDispatch must be used within a CartProvider'); + } + return context; +} + +function useCart() { + return [useCartState(), useCartDispatch()]; +} + +export { CartProvider, useCart, useCartState, useCartDispatch }; diff --git a/apps/with-react-lazy/src/app/modules/cart/cart.reducer.js b/apps/with-react-lazy/src/app/modules/cart/cart.reducer.js new file mode 100644 index 0000000..a4a7154 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/cart.reducer.js @@ -0,0 +1,79 @@ +import { ADD_TO_CART, REMOVE_FROM_CART, RESTORE_CART } from './cart.actions'; + +export const initialState = { + articles: {}, + total: 0, +}; + +export const cartReducer = (state, action) => { + switch (action.type) { + case RESTORE_CART: { + return { ...state, articles: action.articles, total: action.total }; + } + + case ADD_TO_CART: { + const { id } = action.article; + + // It doesn't already exist in the cart articles + if (!state.articles[id]) { + return { + ...state, + articles: { ...state.articles, [id]: action.article }, + total: state.total + action.article.price, + }; + } + + // Now, we know we have at least one occurrence of the current article in the cart + const occurrences = state.articles[id].occurrences; + + const incrementedArticle = { + ...action.article, + // if it's undefined we haven't set it yet because we only have one, fallback on 2 + occurrences: occurrences ? occurrences + 1 : 2, + }; + + return { + ...state, + articles: { ...state.articles, [id]: incrementedArticle }, + total: state.total + action.article.price, + }; + } + + case REMOVE_FROM_CART: { + const targetArticle = Object.values(state.articles).find( + article => article.id === action.id, + ); + const targetOccurrences = targetArticle.occurrences; + const isNumber = typeof targetOccurrences === 'number'; + const isSuperiorToOne = targetOccurrences > 1; + const shouldDecrement = isNumber && isSuperiorToOne; + + if (shouldDecrement) { + return { + ...state, + articles: { + ...state.articles, + [action.id]: { + ...targetArticle, + occurrences: targetOccurrences - 1, + }, + }, + total: state.total - targetArticle.price, + }; + } + + return { + ...state, + articles: Object.keys(state.articles).reduce( + (acc, curr) => + action.id === curr ? acc : { ...acc, [curr]: state.articles[curr] }, + {}, + ), + total: state.total - targetArticle.price, + }; + } + default: { + throw new Error(`Unhandled action type: ${action.type}`); + } + } +}; diff --git a/apps/with-react-lazy/src/app/modules/cart/components/cart.component.js b/apps/with-react-lazy/src/app/modules/cart/components/cart.component.js new file mode 100644 index 0000000..721de6b --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/components/cart.component.js @@ -0,0 +1,110 @@ +import React, { useCallback } from 'react'; +import { Link } from 'react-router-dom'; + +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import IconButton from '@material-ui/core/IconButton'; + +import DeleteIcon from '@material-ui/icons/RemoveCircle'; + +import { makeStyles } from '@material-ui/core/styles'; + +import { useCart } from '../cart.context'; +import { removeFromCart } from '../cart.actions'; +import { ROUTES_PATHS_BY_NAMES } from '../../routing/routing.constants'; + +const useStyles = makeStyles({ + card: { + display: 'flex', + flexDirection: 'column', + position: 'sticky', + top: '20px', + }, + cardContent: { + flexGrow: 1, + }, + listItem: { + borderBottom: '1px solid lightgray', + textDecoration: 'none', + color: 'black', + }, +}); + +export function Cart() { + const classes = useStyles(); + const [{ articles, total }, dispatch] = useCart(); + + const removeItemFromList = useCallback( + id => () => dispatch(removeFromCart(id)), + [dispatch], + ); + + return ( + + + + Cart + + + {Object.values(articles).map((article, index) => ( + + + + + + + + + + ))} + + + Total Price: {total} $ + + + + + + + ); +} diff --git a/apps/with-react-lazy/src/app/modules/cart/components/cartLayout.component.js b/apps/with-react-lazy/src/app/modules/cart/components/cartLayout.component.js new file mode 100644 index 0000000..af37943 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/cart/components/cartLayout.component.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import Grid from '@material-ui/core/Grid'; +import { CHILDREN_PROP_TYPES } from '../../../constants/proptypes.constants'; +import { Cart } from './cart.component'; + +export function CartLayout({ children }) { + return ( + + + {children} + + + + + + ); +} + +CartLayout.propTypes = { + children: CHILDREN_PROP_TYPES, +}; diff --git a/apps/with-react-lazy/src/app/modules/checkout/checkout.component.js b/apps/with-react-lazy/src/app/modules/checkout/checkout.component.js new file mode 100644 index 0000000..36609b9 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/checkout/checkout.component.js @@ -0,0 +1,153 @@ +import React, { memo } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Container from '@material-ui/core/Container'; +import Paper from '@material-ui/core/Paper'; +import Stepper from '@material-ui/core/Stepper'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import AddressForm from './components/addressForm.component'; +import PaymentForm from './components/paymentForm.component'; +import Review from './components/review.component'; +import { useStepperForm } from '../../hooks/useStepperForm.hook'; +import { SHIPPING, PAYMENT, REVIEW, steps } from './checkout.constants'; + +const useStyles = makeStyles(theme => ({ + appBar: { + position: 'relative', + borderBottom: `1px solid ${theme.palette.divider}`, + }, + main: { + marginBottom: theme.spacing(4), + }, + paper: { + marginTop: theme.spacing(3), + marginBottom: theme.spacing(3), + padding: theme.spacing(2), + [theme.breakpoints.up('md')]: { + marginTop: theme.spacing(4), + marginBottom: theme.spacing(4), + padding: theme.spacing(3), + }, + }, + stepper: { + padding: theme.spacing(3, 0, 5), + }, + buttons: { + display: 'flex', + justifyContent: 'flex-end', + }, + button: { + marginTop: theme.spacing(3), + marginLeft: theme.spacing(1), + }, +})); + +function getStepContent(step, [formState, setFormState]) { + switch (step) { + case SHIPPING: + return ( + + ); + case PAYMENT: + return ( + + ); + case REVIEW: + return ( + + ); + default: + throw new Error('Unknown step'); + } +} + +export const initialFormState = { + [SHIPPING]: {}, + [PAYMENT]: {}, + [REVIEW]: {}, +}; + +function Checkout() { + const classes = useStyles(); + const [activeStep, setActiveStep] = React.useState(0); + const stepperForm = useStepperForm(initialFormState); + + const handleNext = () => { + setActiveStep(activeStep + 1); + }; + + const handleBack = () => { + setActiveStep(activeStep - 1); + }; + + return ( + + + + Checkout + + + {steps.map(label => ( + + {label} + + ))} + + {activeStep === steps.length ? ( + + + Thank you for your order. + + + Your order number is #2001539. We have emailed your order + confirmation, and will send you an update when your order has + shipped. + + + ) : ( + + {getStepContent(steps[activeStep], stepperForm)} +
+ {activeStep !== 0 && ( + + )} + + +
+
+ )} +
+
+ ); +} + +export default memo(Checkout); diff --git a/apps/with-react-lazy/src/app/modules/checkout/checkout.constants.js b/apps/with-react-lazy/src/app/modules/checkout/checkout.constants.js new file mode 100644 index 0000000..c1959d0 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/checkout/checkout.constants.js @@ -0,0 +1,5 @@ +export const SHIPPING = 'Shipping address'; +export const PAYMENT = 'Payment details'; +export const REVIEW = 'Review your order'; + +export const steps = [SHIPPING, PAYMENT, REVIEW]; diff --git a/apps/with-react-lazy/src/app/modules/checkout/components/addressForm.component.js b/apps/with-react-lazy/src/app/modules/checkout/components/addressForm.component.js new file mode 100644 index 0000000..42becfe --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/checkout/components/addressForm.component.js @@ -0,0 +1,78 @@ +import React, { memo } from 'react'; + +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; + +import { GridTextField } from '../../../components/gridTextField.component'; +import { useStepperFormChild } from '../../../hooks/useStepperFormChild.hook'; + +const INPUTS_CONFIG = { + firstName: { + props: { autoComplete: 'given-name', label: 'First name' }, + gridProps: { xs: 12, sm: 6 }, + }, + lastName: { + props: { autoComplete: 'family-name', label: 'Last name' }, + gridProps: { xs: 12, sm: 6 }, + }, + address1: { + props: { autoComplete: 'shipping-address line-1', label: 'Address line 1' }, + gridProps: { xs: 12 }, + }, + address2: { + props: { autoComplete: 'shipping-address line-2', label: 'Address line 2' }, + gridProps: { xs: 12 }, + }, + city: { + props: { autoComplete: 'shipping address-level2', label: 'City' }, + gridProps: { sm: 6, xs: 12 }, + }, + state: { + props: { label: 'Region/State' }, + gridProps: { sm: 6, xs: 12 }, + }, + zip: { + props: { autoComplete: 'shipping postal-code', label: 'Zip code' }, + gridProps: { sm: 6, xs: 12 }, + }, + country: { + props: { autoComplete: 'shipping country', label: 'Country code' }, + gridProps: { xs: 12, sm: 6 }, + }, +}; + +// eslint-disable-next-line +function AddressForm({ step, setParentState, initialState }) { + const onBlur = useStepperFormChild({ initialState, setParentState, step }); + + return ( + + + Shipping address + + + {Object.keys(INPUTS_CONFIG).map(inputName => ( + + ))} + + + } + label="Use this address for payment details" + /> + + + + ); +} + +export default memo(AddressForm); diff --git a/apps/with-react-lazy/src/app/modules/checkout/components/paymentForm.component.js b/apps/with-react-lazy/src/app/modules/checkout/components/paymentForm.component.js new file mode 100644 index 0000000..25aa7bc --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/checkout/components/paymentForm.component.js @@ -0,0 +1,68 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; + +import { GridTextField } from '../../../components/gridTextField.component'; + +import { useStepperFormChild } from '../../../hooks/useStepperFormChild.hook'; + +const INPUTS_CONFIG = { + cardName: { + props: { + autoComplete: 'cc-name', + label: 'Name on card', + }, + gridProps: { xs: 12, sm: 6 }, + }, + cardNumber: { + props: { + label: 'Card number', + autoComplete: 'cc-number', + }, + gridProps: { xs: 12, sm: 6 }, + }, + expDate: { + props: { autoComplete: 'cc-exp', label: 'Expiry date' }, + gridProps: { xs: 12, sm: 6 }, + }, + cvv: { + props: { + label: 'CVV', + helperText: 'Last three digits on signature strip', + autoComplete: 'cc-csc', + }, + gridProps: { sm: 6, xs: 12 }, + }, +}; + +// eslint-disable-next-line +export default function PaymentForm({ step, setParentState, initialState }) { + const onBlur = useStepperFormChild({ initialState, setParentState, step }); + + return ( + + + Payment method + + + {Object.keys(INPUTS_CONFIG).map(inputName => ( + + ))} + + } + label="Remember credit card details for next time" + /> + + + + ); +} diff --git a/apps/with-react-lazy/src/app/modules/checkout/components/review.component.js b/apps/with-react-lazy/src/app/modules/checkout/components/review.component.js new file mode 100644 index 0000000..48f9372 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/checkout/components/review.component.js @@ -0,0 +1,118 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import Grid from '@material-ui/core/Grid'; + +import { useCart } from '../../cart/cart.context'; +import { PAYMENT, SHIPPING } from '../checkout.constants'; + +const useStyles = makeStyles(theme => ({ + listItem: { + padding: theme.spacing(1, 0), + }, + total: { + fontWeight: 700, + }, + title: { + marginTop: theme.spacing(2), + }, +})); +const defaultObject = {}; + +export const getShippingState = state => { + if (!state[SHIPPING]) return defaultObject; + return state[SHIPPING]; +}; + +export const getPaymentState = state => { + if (!state[PAYMENT]) return defaultObject; + return state[PAYMENT]; +}; + +export default function Review({ formState }) { + const classes = useStyles(); + const [{ articles, total }] = useCart(); + const { firstName, lastName, address1, address2, city, state, zip, country } = + getShippingState(formState); + const { cardName, cardNumber, expDate } = getPaymentState(formState); + + console.log(articles, formState); + + return ( + + + Order summary + + + {Object.values(articles).map(article => ( + + + + $ + {article.occurrences + ? article.occurrences * article.price + : article.price} + + + ))} + + + + + ${total} + + + + + + + {SHIPPING} + + + {firstName} {lastName} + + + {[address1, address2, city, state, zip, country].join(', ')} + + + + + {PAYMENT} + + + + Card Holder + + + {cardName} + + + Card Number + + + {cardNumber} + + + Expires + + + {expDate} + + + + + + ); +} + +Review.propTypes = { + formState: PropTypes.shape({}).isRequired, +}; diff --git a/apps/with-react-lazy/src/app/modules/routing/__tests__/rooting.hooks.spec.js b/apps/with-react-lazy/src/app/modules/routing/__tests__/rooting.hooks.spec.js new file mode 100644 index 0000000..de5932c --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/routing/__tests__/rooting.hooks.spec.js @@ -0,0 +1,142 @@ +// import React from 'react'; +// import { render, unmountComponentAtNode } from 'react-dom'; +// import { act } from 'react-dom/test-utils'; +// import { useHistory, useLocation } from 'react-router-dom'; + +// import { isUserConnected } from '../../user/user.selectors'; +// import { ROUTES_PATHS_BY_NAMES } from '../routing.constants'; + +// import { useLoginRedirect } from '../routing.hooks'; + +// jest.mock('react-router-dom', () => ({ +// useHistory: jest.fn().mockReturnValue({ push: jest.fn() }), +// useLocation: jest.fn().mockReturnValue({ pathname: 'foo' }), +// })); +// jest.mock('../../user/user.context.js', () => ({ useUserState: jest.fn() })); +// jest.mock('../../user/user.selectors.js', () => ({ +// isUserConnected: jest.fn().mockReturnValue(false), +// })); + +// // Create a wrapper to execute the hook inside a react component function body +// const ShowCase = () => { +// useLoginRedirect(); + +// return
; +// }; + +// describe('useLoginRedirect', () => { +// beforeEach(() => { +// jest.clearAllMocks(); +// }); + +// describe('calls proper hooks', () => { +// beforeEach(() => { +// shallow(); +// }); + +// it('should call isUserConnected', () => { +// expect(isUserConnected).toHaveBeenCalled(); +// }); + +// it('should call useLocation', () => { +// expect(useLocation).toHaveBeenCalled(); +// }); + +// it('should call useHistory', () => { +// expect(useHistory).toHaveBeenCalled(); +// }); +// }); + +// describe('when not connected', () => { +// describe('on home page', () => { +// it('should not call push', () => { +// const push = jest.fn(); +// useHistory.mockReturnValueOnce({ push }); +// useLocation.mockReturnValueOnce({ pathname: ROUTES_PATHS_BY_NAMES.checkout }); +// shallow(); + +// expect(push).not.toBeCalled(); +// }); +// }); +// }); + +// describe('effect', () => { +// const push = jest.fn(); +// let container = null; + +// beforeEach(() => { +// useHistory.mockReturnValueOnce({ push }); +// container = document.createElement('div'); +// document.body.appendChild(container); +// }); + +// afterEach(() => { +// unmountComponentAtNode(container); +// container.remove(); +// container = null; +// }); + +// describe('when not connected', () => { +// it('should push to login page on checkout page', async () => { +// useLocation.mockReturnValueOnce({ pathname: ROUTES_PATHS_BY_NAMES.checkout }); + +// await act(async () => { +// render(, container); +// }); + +// expect(push).toBeCalledWith(ROUTES_PATHS_BY_NAMES.login); +// }); + +// it('should not push on home page', async () => { +// useLocation.mockReturnValueOnce({ pathname: ROUTES_PATHS_BY_NAMES.home }); + +// await act(async () => { +// render(, container); +// }); + +// expect(push).not.toBeCalled(); +// }); +// }); + +// describe('when connected', () => { +// beforeEach(() => { +// isUserConnected.mockReturnValueOnce(true); +// }); + +// it('should not call push on checkout page', async () => { +// useLocation.mockReturnValueOnce({ pathname: ROUTES_PATHS_BY_NAMES.checkout }); + +// await act(async () => { +// render(, container); +// }); + +// expect(push).not.toBeCalled(); +// }); + +// it('should not call push on home page', async () => { +// useLocation.mockReturnValueOnce({ pathname: ROUTES_PATHS_BY_NAMES.home }); + +// await act(async () => { +// render(, container); +// }); + +// expect(push).not.toBeCalled(); +// }); + +// it('should push from login page to home page', async () => { +// useLocation.mockReturnValueOnce({ pathname: ROUTES_PATHS_BY_NAMES.login }); + +// await act(async () => { +// render(, container); +// }); + +// expect(push).toBeCalledWith(ROUTES_PATHS_BY_NAMES.home); +// }); +// }); +// }); +// }); +describe('placeholder', () => { + it('should pass', () => { + expect(true).toBe(true); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/routing/components/routes.component.js b/apps/with-react-lazy/src/app/modules/routing/components/routes.component.js new file mode 100644 index 0000000..7aef6e0 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/routing/components/routes.component.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Switch, Route } from 'react-router-dom'; + +import { CartProvider } from '../../cart/cart.context'; +import { ArticlesProvider } from '../../articles/articles.context'; + +import { HomePage } from '../../../pages/home.page'; +import { ArticlePage } from '../../../pages/article.page'; +import { AboutPage } from '../../../pages/about.page'; +import { LoginPage } from '../../../pages/login.page'; +import { ContactPage } from '../../../pages/contact.page'; +import { CheckoutPage } from '../../../pages/checkout.page'; + +import { ROUTES_PATHS_BY_NAMES } from '../routing.constants'; +import { useLoginRedirect } from '../routing.hooks'; + +export function AppRoutes() { + useLoginRedirect(); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/with-react-lazy/src/app/modules/routing/routing.constants.js b/apps/with-react-lazy/src/app/modules/routing/routing.constants.js new file mode 100644 index 0000000..b5e0e63 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/routing/routing.constants.js @@ -0,0 +1,10 @@ +export const ROUTES_PATHS_BY_NAMES = { + home: '/', + login: '/login', + about: '/about', + contact: '/contact', + article: '/articles/:id', + checkout: '/checkout', +}; + +export const PROTECTED_PATHS = [ROUTES_PATHS_BY_NAMES.checkout]; diff --git a/apps/with-react-lazy/src/app/modules/routing/routing.hooks.js b/apps/with-react-lazy/src/app/modules/routing/routing.hooks.js new file mode 100644 index 0000000..d47402b --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/routing/routing.hooks.js @@ -0,0 +1,35 @@ +import { useEffect, useState, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { useUserState } from '../user/user.context'; +import { PROTECTED_PATHS, ROUTES_PATHS_BY_NAMES } from './routing.constants'; +import { isUserConnected } from '../user/user.selectors'; + +const { login: loginPath, home: homePath } = ROUTES_PATHS_BY_NAMES; + +export const useLoginRedirect = () => { + const state = useUserState(); + const isConnected = isUserConnected(state); + const { pathname } = useLocation(); + const { push } = useHistory(); + + const [initialRoute, setInitialRoute] = useState( + pathname === loginPath ? homePath : pathname, + ); + + const isProtectedRoute = PROTECTED_PATHS.includes(pathname); + const isLoginRoute = useMemo(() => pathname === loginPath, [pathname]); + + useEffect(() => { + if (isConnected && isLoginRoute) { + push(initialRoute); + } + }, [isConnected, push, isLoginRoute, initialRoute]); + + useEffect(() => { + if (!isConnected && isProtectedRoute) { + setInitialRoute(pathname); + push(loginPath); + } + }, [isConnected, push, pathname, isProtectedRoute]); +}; diff --git a/apps/with-react-lazy/src/app/modules/user/__tests__/user.actions.spec.js b/apps/with-react-lazy/src/app/modules/user/__tests__/user.actions.spec.js new file mode 100644 index 0000000..c5971be --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/__tests__/user.actions.spec.js @@ -0,0 +1,45 @@ +import { signIn, signOut } from '@react-course-v2/api'; +import { LOGIN, login, LOGOUT, logout } from '../user.actions'; + +const user = { id: 'xyz', mail: 'foo@bar.com', name: 'Foo Bar' }; + +jest.mock('@react-course-v2/api'); + +describe('user.actions', () => { + let dispatch, getState; + beforeEach(() => { + jest.clearAllMocks(); + dispatch = jest.fn(); + getState = jest.fn(); + signIn.mockResolvedValue(user); + signOut.mockReturnValue(user); + }); + + describe('login', () => { + it('should dispatch LOGIN', async () => { + await login('foo', 'bar')(dispatch, getState); + return expect(dispatch).toBeCalledWith({ type: LOGIN, user }); + }); + + it('should call signIn', async () => { + await login('foo', 'bar')(dispatch, getState); + return expect(signIn).toBeCalledWith(['foo', 'bar']); + }); + }); + + describe('logout', () => { + beforeEach(() => { + getState.mockReturnValueOnce({ user }); + }); + + it('should dispatch LOGOUT', async () => { + await logout()(dispatch, getState); + return expect(dispatch).toBeCalledWith({ type: LOGOUT, user }); + }); + + it('should call signOut', async () => { + await logout()(dispatch, getState); + return expect(signOut).toBeCalledWith(user); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/user/__tests__/user.context.spec.js b/apps/with-react-lazy/src/app/modules/user/__tests__/user.context.spec.js new file mode 100644 index 0000000..9fcfc5e --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/__tests__/user.context.spec.js @@ -0,0 +1,23 @@ +import React from 'react'; + +import { useUser, useUserState, useUserDispatch } from '../user.context'; + +describe('user.context', () => { + describe('useUserDispatch', () => { + it('should be defined', () => { + expect(typeof useUserDispatch).toBe('function'); + }); + }); + + describe('useUserState', () => { + it('should be defined', () => { + expect(typeof useUserState).toBe('function'); + }); + }); + + describe('useUser', () => { + it('should be defined', () => { + expect(typeof useUser).toBe('function'); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/user/__tests__/user.reducer.spec.js b/apps/with-react-lazy/src/app/modules/user/__tests__/user.reducer.spec.js new file mode 100644 index 0000000..3a23a5d --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/__tests__/user.reducer.spec.js @@ -0,0 +1,29 @@ +import { LOGIN, LOGOUT } from '../user.actions'; +import { userReducer, initialState } from '../user.reducer'; + +describe('user.reducer', () => { + describe('LOGIN', () => { + it('should set user in the state', () => { + expect( + userReducer(initialState, { type: LOGIN, user: { id: 'foo' } }), + ).toMatchObject({ + ...initialState, + user: { id: 'foo' }, + }); + }); + }); + + describe('LOGOUT', () => { + it('should set user to null', () => { + const state = { + ...initialState, + user: { id: 'foo' }, + }; + + expect(userReducer(state, { type: LOGOUT, id: 'foo' })).toMatchObject({ + ...state, + user: null, + }); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/user/__tests__/user.selectors.spec.js b/apps/with-react-lazy/src/app/modules/user/__tests__/user.selectors.spec.js new file mode 100644 index 0000000..5a96502 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/__tests__/user.selectors.spec.js @@ -0,0 +1,19 @@ +import { getUser, isUserConnected } from '../user.selectors'; + +describe('user.selectors', () => { + describe('getUser', () => { + it('should return user', () => { + expect(getUser({ user: { foo: 'bar' } })).toEqual({ foo: 'bar' }); + }); + }); + + describe('isUserConnected', () => { + it('should return false when user is falsy', () => { + expect(isUserConnected({ user: null })).toBeFalsy(); + }); + + it('should return true when user is truthy', () => { + expect(isUserConnected({ user: {} })).toBeTruthy(); + }); + }); +}); diff --git a/apps/with-react-lazy/src/app/modules/user/components/login.component.js b/apps/with-react-lazy/src/app/modules/user/components/login.component.js new file mode 100644 index 0000000..ddfbd9b --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/components/login.component.js @@ -0,0 +1,123 @@ +import React from 'react'; + +import Avatar from '@material-ui/core/Avatar'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Link from '@material-ui/core/Link'; +import Grid from '@material-ui/core/Grid'; +import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; +import Typography from '@material-ui/core/Typography'; +import { makeStyles } from '@material-ui/core/styles'; + +import { useUserDispatch } from '../user.context'; +import { login } from '../user.actions'; +import { useInput } from '../../../hooks/useInput.hook'; +import { Container } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + paper: { + marginTop: theme.spacing(8), + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + avatar: { + margin: theme.spacing(1), + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing(1), + }, + submit: { + margin: theme.spacing(3, 0, 2), + }, +})); + +export const Login = () => { + const classes = useStyles(); + const dispatch = useUserDispatch(); + + const [email, handleEmailChange] = useInput(); + const [password, handlePasswordChange] = useInput(); + + const handleSubmit = e => { + e.preventDefault(); + dispatch(login(email, password)); + }; + + return ( + +
+ + + + + Sign in + +
+ + + } + label="Remember me" + /> + + + + + Forgot password? + + + + + {"Don't have an account? Sign Up"} + + + + +
+
+ ); +}; diff --git a/apps/with-react-lazy/src/app/modules/user/user.actions.js b/apps/with-react-lazy/src/app/modules/user/user.actions.js new file mode 100644 index 0000000..4089a18 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/user.actions.js @@ -0,0 +1,35 @@ +import { signIn, signOut } from '@react-course-v2/api'; +import { getUser } from './user.selectors'; + +export const LOGIN = 'user/LOGIN'; +export const LOGOUT = 'user/LOGOUT'; + +const encryptUserCredentials = (...args) => [...args]; + +export const login = (email, password) => async dispatch => { + try { + const encryptedUser = encryptUserCredentials(email, password); + const user = await signIn(encryptedUser); + + localStorage.setItem('user', JSON.stringify(user)); + + return dispatch({ type: LOGIN, user }); + } catch (error) { + dispatch({ type: LOGIN, error }); + } +}; + +export const logout = () => async (dispatch, getState) => { + try { + const user = getUser(getState()); + if (!user) return; + + localStorage.removeItem('user'); + + await signOut(user); + + return dispatch({ type: LOGOUT, user }); + } catch (error) { + dispatch({ type: LOGOUT, error }); + } +}; diff --git a/apps/with-react-lazy/src/app/modules/user/user.context.js b/apps/with-react-lazy/src/app/modules/user/user.context.js new file mode 100644 index 0000000..de6b9bf --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/user.context.js @@ -0,0 +1,54 @@ +import React from 'react'; + +import { userReducer, initialState } from './user.reducer'; + +import { dispatchThunk } from '../../utils/context.utils'; +import { CHILDREN_PROP_TYPES } from '../../constants/proptypes.constants'; +import { usePersistedUser } from './user.hooks'; + +const UserStateContext = React.createContext(); +const UserDispatchContext = React.createContext(); + +const UserProvider = ({ children }) => { + const user = usePersistedUser(); + const updatedState = user && { user }; + const [state, dispatch] = React.useReducer( + userReducer, + updatedState || initialState, + ); + const getState = React.useCallback(() => state, [state]); + + return ( + + + {children} + + + ); +}; + +UserProvider.propTypes = { + children: CHILDREN_PROP_TYPES, +}; + +function useUserState() { + const context = React.useContext(UserStateContext); + if (context === undefined) { + throw new Error('useUserState must be used within a UserProvider'); + } + return context; +} + +function useUserDispatch() { + const context = React.useContext(UserDispatchContext); + if (context === undefined) { + throw new Error('useUserDispatch must be used within a UserProvider'); + } + return context; +} + +function useUser() { + return [useUserState(), useUserDispatch()]; +} + +export { UserProvider, useUser, useUserState, useUserDispatch }; diff --git a/apps/with-react-lazy/src/app/modules/user/user.hooks.js b/apps/with-react-lazy/src/app/modules/user/user.hooks.js new file mode 100644 index 0000000..ba4cad3 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/user.hooks.js @@ -0,0 +1,5 @@ +export const usePersistedUser = () => { + // You would normally validate the user token here + // and set a new one in case it is not valid anymore + return localStorage.getItem('user'); +}; diff --git a/apps/with-react-lazy/src/app/modules/user/user.reducer.js b/apps/with-react-lazy/src/app/modules/user/user.reducer.js new file mode 100644 index 0000000..bb50cc5 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/user.reducer.js @@ -0,0 +1,25 @@ +import { LOGIN, LOGOUT } from './user.actions'; + +export const initialState = { + user: null, +}; + +export const userReducer = (state, action) => { + if (action.error) { + return { ...state, error: action.error }; + } + + switch (action.type) { + case LOGIN: { + return { ...state, user: action.user }; + } + + case LOGOUT: { + return { ...state, user: null }; + } + + default: { + throw new Error(`Unhandled action type: ${action.type}`); + } + } +}; diff --git a/apps/with-react-lazy/src/app/modules/user/user.selectors.js b/apps/with-react-lazy/src/app/modules/user/user.selectors.js new file mode 100644 index 0000000..4d798c1 --- /dev/null +++ b/apps/with-react-lazy/src/app/modules/user/user.selectors.js @@ -0,0 +1,2 @@ +export const isUserConnected = ({ user }) => !!user; +export const getUser = ({ user }) => user; diff --git a/apps/with-react-lazy/src/app/pages/about.page.js b/apps/with-react-lazy/src/app/pages/about.page.js new file mode 100644 index 0000000..826391c --- /dev/null +++ b/apps/with-react-lazy/src/app/pages/about.page.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import Button from '@material-ui/core/Button'; +import Box from '@material-ui/core/Box'; + +import { Layout } from '../components/layout.component'; + +export const AboutPage = () => { + return ( + + +

About

+ +
+
+ ); +}; diff --git a/apps/with-react-lazy/src/app/pages/article.page.js b/apps/with-react-lazy/src/app/pages/article.page.js new file mode 100644 index 0000000..09193c7 --- /dev/null +++ b/apps/with-react-lazy/src/app/pages/article.page.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { Link, useParams } from 'react-router-dom'; + +import Button from '@material-ui/core/Button'; +import Box from '@material-ui/core/Box'; + +import { Layout } from '../components/layout.component'; +import { Article } from '../modules/articles/components/article.component'; +import { CartLayout } from '../modules/cart/components/cartLayout.component'; + +export const ArticlePage = () => { + const { id } = useParams(); + + return ( + + +

Article {id}

+ +
+ +
+ + + ); +}; diff --git a/apps/with-react-lazy/src/app/pages/checkout.page.js b/apps/with-react-lazy/src/app/pages/checkout.page.js new file mode 100644 index 0000000..a510d80 --- /dev/null +++ b/apps/with-react-lazy/src/app/pages/checkout.page.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import { Layout } from '../components/layout.component'; +import Checkout from '../modules/checkout/checkout.component'; + +export const CheckoutPage = () => { + return ( + + + + ); +}; diff --git a/apps/with-react-lazy/src/app/pages/contact.page.js b/apps/with-react-lazy/src/app/pages/contact.page.js new file mode 100644 index 0000000..d786446 --- /dev/null +++ b/apps/with-react-lazy/src/app/pages/contact.page.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import Button from '@material-ui/core/Button'; +import Box from '@material-ui/core/Box'; + +import { Layout } from '../components/layout.component'; + +export const ContactPage = () => { + return ( + + +

Contact

+ +
+
+ ); +}; diff --git a/apps/with-react-lazy/src/app/pages/home.page.js b/apps/with-react-lazy/src/app/pages/home.page.js new file mode 100644 index 0000000..9ecdf58 --- /dev/null +++ b/apps/with-react-lazy/src/app/pages/home.page.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import { Layout } from '../components/layout.component'; +import { ArticlesList } from '../modules/articles/components/articlesList.component'; +import { CartLayout } from '../modules/cart/components/cartLayout.component'; + +export const HomePage = () => { + return ( + +

Home Page

+ + + +
+ ); +}; diff --git a/apps/with-react-lazy/src/app/pages/login.page.js b/apps/with-react-lazy/src/app/pages/login.page.js new file mode 100644 index 0000000..97c2092 --- /dev/null +++ b/apps/with-react-lazy/src/app/pages/login.page.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import { Layout } from '../components/layout.component'; +import { Login } from '../modules/user/components/login.component'; + +export const LoginPage = () => { + return ( + + + + ); +}; diff --git a/apps/with-react-lazy/src/app/utils/context.utils.js b/apps/with-react-lazy/src/app/utils/context.utils.js new file mode 100644 index 0000000..8998f4c --- /dev/null +++ b/apps/with-react-lazy/src/app/utils/context.utils.js @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; + +export const dispatchThunk = (dispatch, getState) => param => { + if (typeof param === 'function') { + return param(dispatch, getState); + } + + return dispatch(param); +}; + +export const useSelector = ( + useReducerHook, + selector = state => state, + { shouldFetch = false, fetchCondition = element => !!element, fetchAction }, +) => { + if (!useReducerHook) { + throw new Error( + 'You need to provide the reducer hook of this resource to get its state and dispatch', + ); + } + + const [state, dispatch] = useReducerHook(); + + const selectedValue = selector(state); + + useEffect(() => { + if (shouldFetch && fetchCondition(selectedValue) && fetchAction) { + dispatch(fetchAction()); + } + }, [dispatch, selectedValue, shouldFetch, fetchCondition, fetchAction]); + + return selectedValue; +}; diff --git a/apps/with-react-lazy/src/assets/.gitkeep b/apps/with-react-lazy/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/with-react-lazy/src/assets/README-fr.md b/apps/with-react-lazy/src/assets/README-fr.md new file mode 100644 index 0000000..47b9a3d --- /dev/null +++ b/apps/with-react-lazy/src/assets/README-fr.md @@ -0,0 +1,13 @@ +# With React Lazy/ Ajouter React.lazy à l'application + +| Action | Fichiers | Exports | +| -------- | ---------- | ------- | +| MODIFIER | src/App.js | App | + +## En résumé + +## Pas à pas + +### App.js + +- diff --git a/apps/with-react-lazy/src/assets/README.md b/apps/with-react-lazy/src/assets/README.md new file mode 100644 index 0000000..7ec9cbd --- /dev/null +++ b/apps/with-react-lazy/src/assets/README.md @@ -0,0 +1,11 @@ +# With React Lazy/ Add React.lazy to the application + +| Action | Files | Exports | +| ------ | ---------- | ------- | +| MODIFY | src/App.js | App | + +## TL;DR + +## Step by step + +### App.js diff --git a/apps/with-react-lazy/src/assets/articles.json b/apps/with-react-lazy/src/assets/articles.json new file mode 100644 index 0000000..256e315 --- /dev/null +++ b/apps/with-react-lazy/src/assets/articles.json @@ -0,0 +1,72 @@ +[ + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque nunc sagittis sollicitudin placerat. Maecenas in consectetur magna, egestas auctor massa. Aenean velit nunc, pulvinar id commodo in, ultricies nec velit. Integer efficitur porttitor feugiat. Nulla facilisi. Nulla sit amet mauris eu metus pellentesque faucibus. Nunc mattis dapibus urna at vulputate. Integer lobortis nisl quis arcu sodales sodales. Praesent malesuada et metus non vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nullam diam orci, ornare ut aliquet et, porta eget arcu.", + "id": "O4GVOHgdvjkdivlRxQPlZpV", + "image": "https://picsum.photos/602/800", + "name": "Massive Oak Table", + "price": 1000, + "slug": "massive-oak-table-1975", + "type": "furniture", + "year": "1975" + }, + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque nunc sagittis sollicitudin placerat. Maecenas in consectetur magna, egestas auctor massa. Aenean velit nunc, pulvinar id commodo in, ultricies nec velit. Integer efficitur porttitor feugiat. Nulla facilisi. Nulla sit amet mauris eu metus pellentesque faucibus. Nunc mattis dapibus urna at vulputate. Integer lobortis nisl quis arcu sodales sodales. Praesent malesuada et metus non vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nullam diam orci, ornare ut aliquet et, porta eget arcu.", + "id": "O4GVOg7clwmlRxQPlZpV", + "image": "https://picsum.photos/601/801", + "name": "Toyota Yaris", + "price": 13000, + "slug": "toyota-yaris-2014", + "type": "car", + "year": "2014" + }, + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque nunc sagittis sollicitudin placerat. Maecenas in consectetur magna, egestas auctor massa. Aenean velit nunc, pulvinar id commodo in, ultricies nec velit. Integer efficitur porttitor feugiat. Nulla facilisi. Nulla sit amet mauris eu metus pellentesque faucibus. Nunc mattis dapibus urna at vulputate. Integer lobortis nisl quis arcu sodales sodales. Praesent malesuada et metus non vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nullam diam orci, ornare ut aliquet et, porta eget arcu.", + "id": "O4GVOg7vddfvlRxQPlZpV", + "image": "https://picsum.photos/600/802", + "name": "Siver Ear Rings", + "price": 370, + "slug": "silver-ear-rings-2014", + "type": "jewelry", + "year": "2014" + }, + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque nunc sagittis sollicitudin placerat. Maecenas in consectetur magna, egestas auctor massa. Aenean velit nunc, pulvinar id commodo in, ultricies nec velit. Integer efficitur porttitor feugiat. Nulla facilisi. Nulla sit amet mauris eu metus pellentesque faucibus. Nunc mattis dapibus urna at vulputate. Integer lobortis nisl quis arcu sodales sodales. Praesent malesuada et metus non vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nullam diam orci, ornare ut aliquet et, porta eget arcu.", + "id": "O4GVscjisjcjumlRxQPlZpV", + "image": "https://picsum.photos/603/800", + "name": "Vintage Leather Jacket", + "price": 500, + "slug": "vintage-leather-jacket-1980", + "type": "music", + "year": "1980" + }, + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque nunc sagittis sollicitudin placerat. Maecenas in consectetur magna, egestas auctor massa. Aenean velit nunc, pulvinar id commodo in, ultricies nec velit. Integer efficitur porttitor feugiat. Nulla facilisi. Nulla sit amet mauris eu metus pellentesque faucibus. Nunc mattis dapibus urna at vulputate. Integer lobortis nisl quis arcu sodales sodales. Praesent malesuada et metus non vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nullam diam orci, ornare ut aliquet et, porta eget arcu.", + "id": "YhhG5sldvidvnkkMF5g67juG", + "image": "https://picsum.photos/600/800", + "name": "Renault Megane", + "price": 16000, + "slug": "renault-megane-2016", + "type": "car", + "year": "2016" + }, + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam scelerisque nunc sagittis sollicitudin placerat. Maecenas in consectetur magna, egestas auctor massa. Aenean velit nunc, pulvinar id commodo in, ultricies nec velit. Integer efficitur porttitor feugiat. Nulla facilisi. Nulla sit amet mauris eu metus pellentesque faucibus. Nunc mattis dapibus urna at vulputate. Integer lobortis nisl quis arcu sodales sodales. Praesent malesuada et metus non vestibulum. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nullam diam orci, ornare ut aliquet et, porta eget arcu.", + "id": "ljF9FsldvkslzF5g67juG", + "image": "https://picsum.photos/600/801", + "name": "Mercedes Class A", + "price": 35000, + "slug": "mercedes-class-a-2016", + "type": "car", + "year": "2016" + }, + { + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu hendrerit dolor, at commodo mi. Nam egestas malesuada varius. Praesent eu interdum lorem. Sed diam purus, vulputate et pharetra at, pharetra ut eros. Morbi id ipsum lorem. Aliquam non massa vulputate, sodales nisi ut, tempor mi. Integer pellentesque tempor nunc a vulputate. Praesent eu magna id ante dignissim pharetra. Proin rutrum vulputate ligula, et luctus eros finibus sit amet. Maecenas orci nibh, facilisis quis enim sed, efficitur rhoncus neque. Nunc congue commodo dui convallis bibendum. Aliquam erat volutpat.", + "id": "ljF9dkvdknknnOJQMFTeqcBmS", + "image": "https://picsum.photos/601/800", + "name": "Honda Civic", + "price": 20000, + "slug": "honda-civic-2017", + "type": "car", + "year": "2017" + } +] diff --git a/apps/with-react-lazy/src/environments/environment.prod.ts b/apps/with-react-lazy/src/environments/environment.prod.ts new file mode 100644 index 0000000..c966979 --- /dev/null +++ b/apps/with-react-lazy/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/apps/with-react-lazy/src/environments/environment.ts b/apps/with-react-lazy/src/environments/environment.ts new file mode 100644 index 0000000..7ed8376 --- /dev/null +++ b/apps/with-react-lazy/src/environments/environment.ts @@ -0,0 +1,6 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// When building for production, this file is replaced with `environment.prod.ts`. + +export const environment = { + production: false, +}; diff --git a/apps/with-react-lazy/src/favicon.ico b/apps/with-react-lazy/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA + + + + WithReactLazy + + + + + + +
+ + diff --git a/apps/with-react-lazy/src/main.tsx b/apps/with-react-lazy/src/main.tsx new file mode 100644 index 0000000..372956b --- /dev/null +++ b/apps/with-react-lazy/src/main.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ExerciseContainer } from '@react-course-v2/course-hints'; +import { createMuiTheme, ThemeProvider } from '@material-ui/core'; + +import App from './app/app'; + +const theme = createMuiTheme({ + spacing: n => n * 4, +}); + +ReactDOM.render( + + + + + , + document.getElementById('root'), +); diff --git a/apps/with-react-lazy/src/polyfills.ts b/apps/with-react-lazy/src/polyfills.ts new file mode 100644 index 0000000..2adf3d0 --- /dev/null +++ b/apps/with-react-lazy/src/polyfills.ts @@ -0,0 +1,7 @@ +/** + * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`. + * + * See: https://github.com/zloirock/core-js#babel + */ +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; diff --git a/apps/with-react-lazy/src/styles.css b/apps/with-react-lazy/src/styles.css new file mode 100644 index 0000000..e2ab3c1 --- /dev/null +++ b/apps/with-react-lazy/src/styles.css @@ -0,0 +1,14 @@ +/* You can add global styles to this file, and also import other style files */ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/apps/with-react-lazy/tsconfig.app.json b/apps/with-react-lazy/tsconfig.app.json new file mode 100644 index 0000000..71adee6 --- /dev/null +++ b/apps/with-react-lazy/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/apps/with-react-lazy/tsconfig.json b/apps/with-react-lazy/tsconfig.json new file mode 100644 index 0000000..3cef3de --- /dev/null +++ b/apps/with-react-lazy/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/apps/with-react-lazy/tsconfig.spec.json b/apps/with-react-lazy/tsconfig.spec.json new file mode 100644 index 0000000..fee141b --- /dev/null +++ b/apps/with-react-lazy/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ], + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ] +} diff --git a/nx.json b/nx.json index a970219..14e5a97 100644 --- a/nx.json +++ b/nx.json @@ -98,6 +98,13 @@ "react-course-demo-e2e": { "tags": [], "implicitDependencies": ["react-course-demo"] + }, + "with-react-lazy": { + "tags": [] + }, + "with-react-lazy-e2e": { + "tags": [], + "implicitDependencies": ["with-react-lazy"] } }, "targetDependencies": { diff --git a/workspace.json b/workspace.json index cd182bb..1f4c618 100644 --- a/workspace.json +++ b/workspace.json @@ -1181,6 +1181,108 @@ } } } + }, + "with-react-lazy": { + "root": "apps/with-react-lazy", + "sourceRoot": "apps/with-react-lazy/src", + "projectType": "application", + "architect": { + "build": { + "builder": "@nrwl/web:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/with-react-lazy", + "index": "apps/with-react-lazy/src/index.html", + "main": "apps/with-react-lazy/src/main.tsx", + "polyfills": "apps/with-react-lazy/src/polyfills.ts", + "tsConfig": "apps/with-react-lazy/tsconfig.app.json", + "assets": [ + "apps/with-react-lazy/src/favicon.ico", + "apps/with-react-lazy/src/assets" + ], + "styles": ["apps/with-react-lazy/src/styles.css"], + "scripts": [], + "webpackConfig": "@nrwl/react/plugins/webpack" + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "apps/with-react-lazy/src/environments/environment.ts", + "with": "apps/with-react-lazy/src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + } + ] + } + } + }, + "serve": { + "builder": "@nrwl/web:dev-server", + "options": { + "buildTarget": "with-react-lazy:build", + "hmr": true + }, + "configurations": { + "production": { + "buildTarget": "with-react-lazy:build:production", + "hmr": false + } + } + }, + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["apps/with-react-lazy/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/apps/with-react-lazy"], + "options": { + "jestConfig": "apps/with-react-lazy/jest.config.js", + "passWithNoTests": true + } + } + } + }, + "with-react-lazy-e2e": { + "root": "apps/with-react-lazy-e2e", + "sourceRoot": "apps/with-react-lazy-e2e/src", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@nrwl/cypress:cypress", + "options": { + "cypressConfig": "apps/with-react-lazy-e2e/cypress.json", + "tsConfig": "apps/with-react-lazy-e2e/tsconfig.e2e.json", + "devServerTarget": "with-react-lazy:serve" + }, + "configurations": { + "production": { + "devServerTarget": "with-react-lazy:serve:production" + } + } + }, + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["apps/with-react-lazy-e2e/**/*.{js,ts}"] + } + } + } } } }