diff --git a/exercises/exercise-1/README.md b/exercises/exercise-1/README.md deleted file mode 100644 index 0b9e5e6..0000000 --- a/exercises/exercise-1/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# 1/ Fetching, persisting data locally and list rendering - -| Action | Files | Exports | -| ------ | ---------- | ------- | -| MODIFY | src/App.js | App | - -## TL;DR - -Build the JSX: Create a defined markup structure. - -HTML desired output - -```html -
-

Foo

-
-

bar

- -
-
-``` - -## Step by step - -### App.js - -- Fetch the articles: use a combination of useState and useEffect to perform the async call and persist the data. You can import the asynchronous method **getArticles** from the utils folder. -- Update the JSX: iterate over the articles using .map to display the item name in the `
  • ` -- `yarn test 1` diff --git a/exercises/exercise-1/src/App.js b/exercises/exercise-1/src/App.js deleted file mode 100644 index 4f883a2..0000000 --- a/exercises/exercise-1/src/App.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export default function App() { - return ( -
    -

    Home Page

    -
    - ); -} diff --git a/exercises/exercise-1/src/__hints__/dialog.js b/exercises/exercise-1/src/__hints__/dialog.js deleted file mode 100644 index a73e046..0000000 --- a/exercises/exercise-1/src/__hints__/dialog.js +++ /dev/null @@ -1,149 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; - -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; - -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import emoji from 'emoji-dictionary'; - -import instructionsMd from './instructions.md'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-1/src/__hints__/instructions.md b/exercises/exercise-1/src/__hints__/instructions.md deleted file mode 100644 index 0b9e5e6..0000000 --- a/exercises/exercise-1/src/__hints__/instructions.md +++ /dev/null @@ -1,32 +0,0 @@ -# 1/ Fetching, persisting data locally and list rendering - -| Action | Files | Exports | -| ------ | ---------- | ------- | -| MODIFY | src/App.js | App | - -## TL;DR - -Build the JSX: Create a defined markup structure. - -HTML desired output - -```html -
    -

    Foo

    -
    -

    bar

    -
      -
    • baz
    • -
    • boz
    • -
    -
    -
    -``` - -## Step by step - -### App.js - -- Fetch the articles: use a combination of useState and useEffect to perform the async call and persist the data. You can import the asynchronous method **getArticles** from the utils folder. -- Update the JSX: iterate over the articles using .map to display the item name in the `
  • ` -- `yarn test 1` diff --git a/exercises/exercise-1/src/__tests__/App.spec.js b/exercises/exercise-1/src/__tests__/App.spec.js deleted file mode 100644 index 20b4a9e..0000000 --- a/exercises/exercise-1/src/__tests__/App.spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -import MockedArticles from '../../../fixtures/articles.json'; - -import * as ApiUtils from '../utils/api.utils'; - -let wrapper; -const emptyArray = []; - -ApiUtils.getArticles = jest.fn().mockResolvedValue(MockedArticles); - -jest.mock('react', () => - global.mockReactWithHooks({ effect: true, state: true }), -); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - useEffect.mockClear(); - useState.mockClear(); - wrapper = getWrapper(); -}); - -xdescribe('App', () => { - describe('State checks', () => { - // Don't test Jest mocks or React, it is useless - // This tests that our react mock is working, useless - // const [initialsArticles] = useState.mock.results[0].value; - // expect(initialsArticles).toEqual(emptyArray); - - it('should call useState with an empty array', () => { - expect(useState).toHaveBeenCalledWith(emptyArray); - }); - }); - - describe('Effects checks', () => { - it('should call useEffect with a function and an empty array', () => { - expect(useEffect).toHaveBeenCalledWith(expect.any(Function), [ - emptyArray, - ]); - }); - - it('should call getArticles once', () => { - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call getArticles once even when re-rendered', () => { - wrapper.update(); - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call setArticles once with MockedArticles', () => { - const [, setArticles] = useState.mock.results[0].value; - expect(setArticles).toHaveBeenCalledWith(MockedArticles); - expect(setArticles).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/exercises/exercise-1/src/index.css b/exercises/exercise-1/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-1/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-1/src/index.js b/exercises/exercise-1/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-1/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-1/src/logo.svg b/exercises/exercise-1/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-1/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-2/README.md b/exercises/exercise-2/README.md deleted file mode 100644 index 0e99d87..0000000 --- a/exercises/exercise-2/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# 2/ Using react-router-dom to create pages - -| Action | Files | Exports | -| ------ | ------------------------- | ------------- | -| Create | src/pages/home.page.js | {HomePage} | -| Create | src/pages/about.page.js | {AboutPage} | -| Create | src/pages/contact.page.js | {ContactPage} | -| Modify | App.js | {App} | - -## TL;DR - -We are going to create a few pages, for now each will display a simple title and link to home. The home page will be composed of the previous `App.js` content in addition to links to the other pages. App.js will now be used for routing, BrowserRouter, Switch and Route components will land here. - -## Step by step - -- Create a "pages" directory, with the files {home,about,contact}.page.js -- In `src/pages/about.page.js` and `src/pages/contact.page.js` files create a div with a h2 and a react-router-dom Link to / -- In `src/pages/home.page.js`, copy everything from _App.js_ and add `` components to about and contact page under the h2 -- In `src/App.js`, use BrowserRouter, Switch and Route like the following example - -```js -const Foo = () => { - return ( - - - - - - - - - - - ) -} -``` diff --git a/exercises/exercise-2/src/App.js b/exercises/exercise-2/src/App.js deleted file mode 100644 index ef40f65..0000000 --- a/exercises/exercise-2/src/App.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { getArticles } from '@react-course-v2/api'; - -const App = () => { - const [articles, setArticles] = useState([]); - - useEffect(() => { - if (articles.length === 0) { - getArticles() - .then(res => setArticles(res)) - .catch(err => console.error(err)); - } - }, [articles]); - - return ( -
    -

    Home Page

    -
    -

    Articles

    -
      - {articles.length > 0 && - articles.map(({ id, name }) => ( -
    • - {name} -
    • - ))} -
    -
    -
    - ); -}; - -export default App; diff --git a/exercises/exercise-2/src/__hints__/dialog.js b/exercises/exercise-2/src/__hints__/dialog.js deleted file mode 100644 index 079a0bc..0000000 --- a/exercises/exercise-2/src/__hints__/dialog.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import instructionsMd from './instructions.md'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; -import emoji from 'emoji-dictionary'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - ['& blockquote']: { - boxShadow: - '0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%)', - borderRadius: '4px', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - padding: '20px', - borderLeft: '5px solid #3f51b5', - }, - - ['& p']: { - lineHeight: '25px', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-2/src/__hints__/instructions.md b/exercises/exercise-2/src/__hints__/instructions.md deleted file mode 100644 index 0e99d87..0000000 --- a/exercises/exercise-2/src/__hints__/instructions.md +++ /dev/null @@ -1,36 +0,0 @@ -# 2/ Using react-router-dom to create pages - -| Action | Files | Exports | -| ------ | ------------------------- | ------------- | -| Create | src/pages/home.page.js | {HomePage} | -| Create | src/pages/about.page.js | {AboutPage} | -| Create | src/pages/contact.page.js | {ContactPage} | -| Modify | App.js | {App} | - -## TL;DR - -We are going to create a few pages, for now each will display a simple title and link to home. The home page will be composed of the previous `App.js` content in addition to links to the other pages. App.js will now be used for routing, BrowserRouter, Switch and Route components will land here. - -## Step by step - -- Create a "pages" directory, with the files {home,about,contact}.page.js -- In `src/pages/about.page.js` and `src/pages/contact.page.js` files create a div with a h2 and a react-router-dom Link to / -- In `src/pages/home.page.js`, copy everything from _App.js_ and add `` components to about and contact page under the h2 -- In `src/App.js`, use BrowserRouter, Switch and Route like the following example - -```js -const Foo = () => { - return ( - - - - - - - - - - - ) -} -``` diff --git a/exercises/exercise-2/src/__tests__/App.spec.js b/exercises/exercise-2/src/__tests__/App.spec.js deleted file mode 100644 index 2685689..0000000 --- a/exercises/exercise-2/src/__tests__/App.spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -import MockedArticles from '../../../fixtures/articles.json'; - -import * as ApiUtils from '../utils/api.utils'; - -let wrapper; -const emptyArray = []; - -ApiUtils.getArticles = jest.fn().mockResolvedValue(MockedArticles); - -jest.mock('react', () => - global.mockReactWithHooks({ effect: true, state: true }), -); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - useEffect.mockClear(); - useState.mockClear(); - wrapper = getWrapper(); -}); - -describe('App', () => { - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(`[data-testid='app']`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect( - wrapper.find(`[data-testid='articles-container']`).exists(), - ).toBeTruthy(); - expect(wrapper.find(`[data-testid='articles-title']`).text()).toBe( - 'Articles', - ); - expect( - wrapper.find(`[data-testid='articles-list']`).exists(), - ).toBeTruthy(); - }); - }); - - describe('State checks', () => { - // Don't test Jest mocks or React, it is useless - // In the following example we test that our react mock is working - // const [initialsArticles] = useState.mock.results[0].value; - // expect(initialsArticles).toEqual(emptyArray); - - it('should call useState with an empty array', () => { - expect(useState).toHaveBeenCalledWith(emptyArray); - }); - }); - - describe('Effects checks', () => { - it('should call useEffect with a function and an empty array', () => { - expect(useEffect).toHaveBeenCalledWith(expect.any(Function), [ - emptyArray, - ]); - }); - - it('should call getArticles once', () => { - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call getArticles once even when re-rendered', () => { - wrapper.update(); - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call setArticles once with MockedArticles', () => { - const [, setArticles] = useState.mock.results[0].value; - expect(setArticles).toHaveBeenCalledWith(MockedArticles); - expect(setArticles).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/exercises/exercise-2/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-2/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index 6d5da45..0000000 --- a/exercises/exercise-2/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App Snapshot should render correctly 1`] = ` -
    -

    - Home Page -

    -
    -

    - Articles -

    -
      -
    -
    -`; diff --git a/exercises/exercise-2/src/index.css b/exercises/exercise-2/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-2/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-2/src/index.js b/exercises/exercise-2/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-2/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-2/src/logo.svg b/exercises/exercise-2/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-2/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-3/README.md b/exercises/exercise-3/README.md deleted file mode 100644 index e70ea84..0000000 --- a/exercises/exercise-3/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# 3/ Wrapping pages, building layout with Material-UI - -| Action | Files | Exports | -| ------ | ---------------------------------- | ------------- | -| Create | src/components/layout.component.js | {Layout} | -| Modify | src/pages/contact.page.js | {ContactPage} | -| Modify | src/pages/about.page.js | {AboutPage} | -| Modify | src/pages/home.page.js | {HomePage} | -| Modify | src/App.js | {App} | - -## TL;DR - -Now we are going to add some structure to the page, we need a page container component that is responsible for displaying the header (navbar) and the body (content) correctly. - -## Step by step - -- See the newly created `src/components` directory, with the file `navbar.component.js` from material-ui examples -- Add a `layout.component.js` to the "components" directory, it will export a function `Layout` and directly return a Fragment holding the Navbar and a Material-UI Container rendering children. -- In each page component, replace the top parent div with the Layout Component, it is the pages container -- In about and contact pages, add a Material-UI Box component to wrap the h2 and the Link. Use a MUI ` - - - - - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-3/src/__hints__/instructions.md b/exercises/exercise-3/src/__hints__/instructions.md deleted file mode 100644 index b9f48a2..0000000 --- a/exercises/exercise-3/src/__hints__/instructions.md +++ /dev/null @@ -1,20 +0,0 @@ -# 3/ Wrapping pages, building layout with Material-UI - -| Action | Files | Exports | -| ------ | ---------------------------------- | ------------- | -| Create | src/components/layout.component.js | {Layout} | -| Modify | src/pages/contact.page.js | {ContactPage} | -| Modify | src/pages/about.page.js | {AboutPage} | -| Modify | src/pages/home.page.js | {HomePage} | -| Modify | src/App.js | {App} | - -## TL;DR - -Now we are going to add some structure to the page, we need a page container component that is responsible for displaying the header (navbar) and the body (content) correctly. - -## Step by step - -- See the newly created `src/components` directory, with the file `navbar.component.js` from material-ui examples -- Add a `layout.component.js` to the "components" directory, it will export a function `Layout` and directly return a Fragment holding the Navbar and a Material-UI Container rendering children. -- In each page component, replace the div parent with the Layout Component, it is the pages container -- In about and contact pages, add a Material-UI Box component to wrap the h2 and the Link. Use a MUI ` - - - - - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-4/src/__hints__/instructions.md b/exercises/exercise-4/src/__hints__/instructions.md deleted file mode 100644 index 21da1fe..0000000 --- a/exercises/exercise-4/src/__hints__/instructions.md +++ /dev/null @@ -1,49 +0,0 @@ -# 4/ Component composition, modules architecture, understanding responsibility - -| Action | Files | Exports | -| ------ | ---------------------------------------------- | -------------- | -| Create | src/modules/articles/articleCard.component.js | {ArticleCard} | -| Create | src/modules/articles/articlesList.component.js | {ArticlesList} | -| Modify | src/pages/home.page.js | {HomePage} | - -## TL;DR - -We will do two things here: - -- display the articles as Cards -- move the articles logic from the home page to a dedicated directory - -It doesn't feel "react" to have our home page holding the articles **fetching and rendering logic**, after all we may need a list of articles on other pages sometimes ? Let's clean the home page from state, effect and **articles** related jsx, and move it into its own place, something called "**Modules**". - -### What are modules - -**Modules** are **features**, it holds **business rules**. - -> The `Layout` component we just created, lies in the `components` directory because it is generic and holds no logic. -> As opposed to components that performs side-effects (like fetching data). -> It doesn't matter if the component is using _useState_ or _useEffect_, what matters is _is it a feature ?_ or more realistically _do I need redux on this?_ -> In short, you will often see **modules/features** directories in react/redux applications, each of them is generally representing a portion of the global store. - -## Step by step - -### src/modules/articles/articleCard.component.js - -#### ArticleCard - -Create `src/modules/articles/articleCard.component.js` from the [Card MUI example](../../../examples/styling/material-ui/card.component.js), **ArticleCard** will get the article (see API /fixtures/articles.json) as prop _article_. -You'll only need to replace some of the _lorem ipsum_, see tests. - -### src/modules/articles/articlesList.component.js - -#### ArticlesList - -Create `src/modules/articles/articlesList.component.js` that exports a function **ArticlesList**. -Then extract the state, effect and the articles markup from the home page and port it to your `ArticlesList`. -Replace the _ul_ with a _Grid Container_, it should be the top element in the **ArticlesList** - -```js - -``` - -Replace the _li_ html markup with the **ArticleCard**. -Use `ArticlesList` inside the home page and everything should look like before. diff --git a/exercises/exercise-4/src/__tests__/App.spec.js b/exercises/exercise-4/src/__tests__/App.spec.js deleted file mode 100644 index 34e1bc5..0000000 --- a/exercises/exercise-4/src/__tests__/App.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -describe('App', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); diff --git a/exercises/exercise-4/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-4/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index 6b23ab8..0000000 --- a/exercises/exercise-4/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App should render correctly 1`] = ` - - - - - - - - - - - - - -`; diff --git a/exercises/exercise-4/src/components/__tests__/__snapshots__/layout.component.spec.js.snap b/exercises/exercise-4/src/components/__tests__/__snapshots__/layout.component.spec.js.snap deleted file mode 100644 index 3de464d..0000000 --- a/exercises/exercise-4/src/components/__tests__/__snapshots__/layout.component.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - -

    - foo -

    -
    -
    -`; diff --git a/exercises/exercise-4/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap b/exercises/exercise-4/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap deleted file mode 100644 index fd43119..0000000 --- a/exercises/exercise-4/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap +++ /dev/null @@ -1,106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    -`; diff --git a/exercises/exercise-4/src/components/__tests__/layout.component.spec.js b/exercises/exercise-4/src/components/__tests__/layout.component.spec.js deleted file mode 100644 index 6add5d5..0000000 --- a/exercises/exercise-4/src/components/__tests__/layout.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Layout } from '../layout.component'; - -let wrapper; - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('NavBar').exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-4/src/components/__tests__/navbar.component.spec.js b/exercises/exercise-4/src/components/__tests__/navbar.component.spec.js deleted file mode 100644 index 3a8f781..0000000 --- a/exercises/exercise-4/src/components/__tests__/navbar.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AppBar, IconButton, Menu, Typography } from '@material-ui/core'; - -import NavBar from '../navbar.component'; - -let wrapper; - -const getWrapper = () => shallow(); - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(AppBar).exists()).toBeTruthy(); - expect(wrapper.find(Typography).text()).toBe('Shopping App'); - }); - }); - - describe('Interaction checks', () => { - it('should sur anchorEl from clicked item', () => { - expect(wrapper.find(Menu).prop('open')).toBeFalsy(); - wrapper.find(IconButton).prop('onClick')({ currentTarget: 'foo' }); - wrapper.update(); - expect(wrapper.find(Menu).prop('open')).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-4/src/components/layout.component.js b/exercises/exercise-4/src/components/layout.component.js deleted file mode 100644 index d8bede0..0000000 --- a/exercises/exercise-4/src/components/layout.component.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-4/src/components/navbar.component.js b/exercises/exercise-4/src/components/navbar.component.js deleted file mode 100644 index a4bcf0d..0000000 --- a/exercises/exercise-4/src/components/navbar.component.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -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'; - -const useStyles = makeStyles(theme => ({ - root: { - flexGrow: 1, - }, - menuButton: { - marginRight: theme.spacing(2), - }, - title: { - flexGrow: 1, - }, -})); - -export default function NavBar() { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - - const handleMenu = event => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    - ); -} diff --git a/exercises/exercise-4/src/constants/proptypes.constants.js b/exercises/exercise-4/src/constants/proptypes.constants.js deleted file mode 100644 index bfe9d9a..0000000 --- a/exercises/exercise-4/src/constants/proptypes.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ - PropTypes.array.isRequired, - PropTypes.object, - PropTypes.element, -]).isRequired; diff --git a/exercises/exercise-4/src/index.css b/exercises/exercise-4/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-4/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-4/src/index.js b/exercises/exercise-4/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-4/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-4/src/logo.svg b/exercises/exercise-4/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-4/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-4/src/pages/__tests__/about.page.spec.js b/exercises/exercise-4/src/pages/__tests__/about.page.spec.js deleted file mode 100644 index 17b23f5..0000000 --- a/exercises/exercise-4/src/pages/__tests__/about.page.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AboutPage } from '../about.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-4/src/pages/__tests__/contact.page.spec.js b/exercises/exercise-4/src/pages/__tests__/contact.page.spec.js deleted file mode 100644 index 4fa3c8f..0000000 --- a/exercises/exercise-4/src/pages/__tests__/contact.page.spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ContactPage } from '../contact.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); -}); diff --git a/exercises/exercise-4/src/pages/__tests__/home.page.spec.js b/exercises/exercise-4/src/pages/__tests__/home.page.spec.js deleted file mode 100644 index e973a8a..0000000 --- a/exercises/exercise-4/src/pages/__tests__/home.page.spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { shallow } from 'enzyme'; - -import { HomePage } from '../home.page'; - -import MockedArticles from '../../../../fixtures/articles.json'; - -import * as ApiUtils from '../../utils/api.utils'; - -let wrapper; -const emptyArray = []; - -ApiUtils.getArticles = jest.fn().mockResolvedValue(MockedArticles); - -jest.mock('react', () => - global.mockReactWithHooks({ effect: true, state: true }), -); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - useEffect.mockClear(); - useState.mockClear(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect( - wrapper.find(`[data-testid='articles-container']`).exists(), - ).toBeTruthy(); - expect(wrapper.find(`[data-testid='articles-title']`).text()).toBe( - 'Articles', - ); - expect( - wrapper.find(`[data-testid='articles-list']`).exists(), - ).toBeTruthy(); - }); - }); - - describe('State checks', () => { - // Don't test Jest mocks or React, it is useless - // In the following example we test that our react mock is working - // const [initialsArticles] = useState.mock.results[0].value; - // expect(initialsArticles).toEqual(emptyArray); - - it('should call useState with an empty array', () => { - expect(useState).toHaveBeenCalledWith(emptyArray); - }); - }); - - describe('Effects checks', () => { - it('should call useEffect with a function and an empty array', () => { - expect(useEffect).toHaveBeenCalledWith(expect.any(Function), [ - emptyArray, - ]); - }); - - it('should call getArticles once', () => { - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call getArticles once even when re-rendered', () => { - wrapper.update(); - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call setArticles once with MockedArticles', () => { - const [, setArticles] = useState.mock.results[0].value; - expect(setArticles).toHaveBeenCalledWith(MockedArticles); - expect(setArticles).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/exercises/exercise-4/src/pages/about.page.js b/exercises/exercise-4/src/pages/about.page.js deleted file mode 100644 index 567e4b9..0000000 --- a/exercises/exercise-4/src/pages/about.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-4/src/pages/contact.page.js b/exercises/exercise-4/src/pages/contact.page.js deleted file mode 100644 index 1e2e74b..0000000 --- a/exercises/exercise-4/src/pages/contact.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-4/src/pages/home.page.js b/exercises/exercise-4/src/pages/home.page.js deleted file mode 100644 index e3ddc1c..0000000 --- a/exercises/exercise-4/src/pages/home.page.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import { Layout } from '../components/layout.component'; - -import { getArticles } from '@react-course-v2/api'; - -export const HomePage = () => { - const [articles, setArticles] = useState([]); - - useEffect(() => { - if (articles.length !== 0) { - return; - } - getArticles().then(setArticles).catch(console.error); - }, [articles]); - - return ( - -

    Home Page

    -
    -

    Articles

    -
      - {articles.length > 0 && - articles.map(({ id, name }) => ( -
    • - {name} -
    • - ))} -
    -
    -
    - ); -}; diff --git a/exercises/exercise-5/README.md b/exercises/exercise-5/README.md deleted file mode 100644 index 796d66d..0000000 --- a/exercises/exercise-5/README.md +++ /dev/null @@ -1,243 +0,0 @@ -# 5/ Event Driven Design and shared store, the Redux philosophy within React Context - -| Action | Files | Exports | -| ------ | ------------------------------------------------------------- | ---------------------------------------------------------------------- | -| Move | src/modules/articles/{->components/}articleCard.component.js | {ArticleCard} | -| Move | src/modules/articles/{->components/}articlesList.component.js | {ArticlesList} | -| Create | src/pages/article.page.js | {ArticlePage} | -| Create | src/modules/articles/articles.actions.js | {addToCart, removeFromCart, ADD_TO_CART, REMOVE_FROM_CART} | -| Create | src/modules/articles/articles.reducer.js | {initialState, articlesReducer} | -| Create | src/modules/articles/articles.context.js | {useArticles, useArticlesState, useArticlesDispatch, ArticlesProvider} | -| Create | src/modules/articles/articles.selectors.js | {useArticlesSelector} | -| Create | src/modules/articles/components/article.component.js | {Article} | -| Modify | src/App.js | {App} | - -## TL;DR - -A little introduction on **actions** - -There are no cryptic things, but it can be exhausting at first because actions can be of many forms. - -- The base action definition: an object with a type property set to a string. -- You can refer to an action while actually you only are talking about the action type => the string constant -- You can refer to an action as an event, like javascript ones, it can be triggered -- You can refer to an action as a function call as it always requires a dispatch call -- You can refer to an action as a thunk, an function that may perform side effects in addition to be able to dispatch action(s) - -```js -// simplest, straight object -const duStuffAction = { type: 'foo', payload: 'bar', baz: 'boz' }; -dispatch(doStuffAction); - -// Methods that return directly an object works almost the same naturally -const doStuff = () = ({ type: 'foo', payload: 'bar', baz: 'boz' }); -dispatch(doStuff()); - -// Also valid -const doStuff = (dispatch, getState) => { - const foo = getFoo(getState()); - dispatch({ type: 'foo', payload: foo, baz: 'boz'}); -} -dispatch(doStuff) - -// Thunk, it is not necessarily async btw -const doStuff = () = async dispatch => { - const baz = await getBoz() - return dispatch({ type: 'foo', payload: 'bar', baz }); -} -await dispatch(doStuff()) - -// real world thunk -const doManyStuffs = () => async (dispatch, getState) => { - try { - const state = getState(); - const userId = getUserId(state); - const isLoggedIn = isConnected(state); - - dispatch({ type: 'any', foo: isConnected ? 'bar' : 'baz' }); - - const anythingResult = await anything(); - fooBar(anythingResult); - - await dispatch({ type: 'thing'}) - return dispatch({ type: 'could', happen: true}); - - } catch(error) { - // handle errors - monitorError(error); - dispatch({ type: 'again?'}); - } -} -dispatch(doManyStuff()) -``` - -## Step by step - -### articles.actions.js - -Create `src/modules/articles/articles.actions.js` - -It should export a `requestArticles` curried function, no parameter on first call and async dispatch on second call. -The body of the function should be an awaited call to the `getArticles` API utils method. -The return statement should call dispatch with an object given properties "type" set to `articles/RECEIVED_ARTICLES` and "articles" set to the resolved promise of `getArticles` call. - -### articles.reducer.js - -Create `src/modules/articles/articles.reducer.js` - -A reducer looks like this: - -```js -import SOME_ACTION_TYPE from './foo.actions'; - -export const initialState = { someMeaningfulKey: null, otherThing: true }; - -export const reducerFoo = (state = initialState, action) => { - switch (action.type) { - case SOME_ACTION_TYPE: - return { - ...state, - someMeaningfulKey: action.payload, - otherThing: action.bar, - }; - default: - return state; - } -}; -``` - -The property you access in **action** are the one you dispatched: - -```js -export const someAction = data => (dispatch, getState) => { - const isBar = isBarSelector(getState()); - return dispatch({ type: SOME_ACTION_TYPE, payload: data, bar: isBar }); -}; -``` - -It should export an **initialState** object - -```js -export const initialState = { - articles: [], -}; -``` - -It should export an **articlesReducer** method - -```js -export const articlesReducer = () => {}; -``` - -Catch the **RECEIVED_ARTICLES** action type and spread the action articles payload in the state. - -### articles.context.js - -Create `src/modules/articles/articles.context.js` -Basically copy [This snippet](https://kentcdodds.com/blog/how-to-use-react-context-effectively#conclusion), our reducer/context implementation is based on it. - -### articles.selectors.js - -Create `src/modules/articles/articles.selectors.js`. In redux a selector is a function taking the state as parameter and returning a value from it, in our case something like: - -```js -const getArticlesSelector = state => state.articles; - -const ArticlesList = () => { - const articles = getArticlesSelector(useArticlesState()); -}; - -// This could be implemented -const ArticlesList = () => { - const articles = useArticlesStateWithSelector(getArticlesSelector); -}; -``` - -But this is not very pretty so lets implement redux useSelector in `src/utils/context.utils.js` at the difference that we need to give it access to state. - -```js -export const useSelector = (useReducerState, selector = state => state) => { - if (!useReducerState) { - throw new Error( - 'You need to provide the reducer State of this resource to get its state and dispatch', - ); - } - - const state = useReducerState(); - - return selector(state); -}; - -// usage -const articles = useSelector(useArticlesState, getArticlesSelector); -``` - -This is great, this is what you would expect from redux, why not. But as e creating custom stateful hooks that we will reuse, why not implement a fetching effect whenever the state doesn't match a condition ? - -```js -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; -}; - -// usage in separate files -export const useArticlesSelector = () => - useSelector(useArticles, ({ articles }) => articles, { - shouldFetch: true, - fetchCondition: articles => articles.length === 0, - fetchAction: requestArticles, - }); - -const articles = useArticlesSelector(); -``` - -### article.component - -We need some basic component to handle displaying the article page from the slug in the url. - -Create `src/modules/articles/components/article.component.js` - -It should Named export `Article` as a function with on prop **id** -It should use your articles selector to get the articles -It should find in the articles the id matching the slug -It should return the article matching the slug or null - -### article.page - -Create `src/pages/article.page.js` - -The page container, use the same format as about/contact page and append the ArticlesList at the end - -### App.js - -Create new route before `/`, you can declare route with parameters this way - -```js - - - -``` - -Wrap the two articles routes in their own Switch while closing the first one on the top. -You should get two siblings Switch. -Wrap the articles' routes' switch in your ArticlesProvider. -You should be able to access your routes by clicking on the cards or directly from their matching url diff --git a/exercises/exercise-5/src/App.js b/exercises/exercise-5/src/App.js deleted file mode 100644 index 1611278..0000000 --- a/exercises/exercise-5/src/App.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { Switch, Route, BrowserRouter as Router } from 'react-router-dom'; - -import { HomePage } from './pages/home.page'; -import { AboutPage } from './pages/about.page'; -import { ContactPage } from './pages/contact.page'; - -export default function App() { - return ( - - - - - - - - - - - - - - ); -} diff --git a/exercises/exercise-5/src/__hints__/dialog.js b/exercises/exercise-5/src/__hints__/dialog.js deleted file mode 100644 index 079a0bc..0000000 --- a/exercises/exercise-5/src/__hints__/dialog.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import instructionsMd from './instructions.md'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; -import emoji from 'emoji-dictionary'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - ['& blockquote']: { - boxShadow: - '0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%)', - borderRadius: '4px', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - padding: '20px', - borderLeft: '5px solid #3f51b5', - }, - - ['& p']: { - lineHeight: '25px', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-5/src/__hints__/instructions.md b/exercises/exercise-5/src/__hints__/instructions.md deleted file mode 100644 index 796d66d..0000000 --- a/exercises/exercise-5/src/__hints__/instructions.md +++ /dev/null @@ -1,243 +0,0 @@ -# 5/ Event Driven Design and shared store, the Redux philosophy within React Context - -| Action | Files | Exports | -| ------ | ------------------------------------------------------------- | ---------------------------------------------------------------------- | -| Move | src/modules/articles/{->components/}articleCard.component.js | {ArticleCard} | -| Move | src/modules/articles/{->components/}articlesList.component.js | {ArticlesList} | -| Create | src/pages/article.page.js | {ArticlePage} | -| Create | src/modules/articles/articles.actions.js | {addToCart, removeFromCart, ADD_TO_CART, REMOVE_FROM_CART} | -| Create | src/modules/articles/articles.reducer.js | {initialState, articlesReducer} | -| Create | src/modules/articles/articles.context.js | {useArticles, useArticlesState, useArticlesDispatch, ArticlesProvider} | -| Create | src/modules/articles/articles.selectors.js | {useArticlesSelector} | -| Create | src/modules/articles/components/article.component.js | {Article} | -| Modify | src/App.js | {App} | - -## TL;DR - -A little introduction on **actions** - -There are no cryptic things, but it can be exhausting at first because actions can be of many forms. - -- The base action definition: an object with a type property set to a string. -- You can refer to an action while actually you only are talking about the action type => the string constant -- You can refer to an action as an event, like javascript ones, it can be triggered -- You can refer to an action as a function call as it always requires a dispatch call -- You can refer to an action as a thunk, an function that may perform side effects in addition to be able to dispatch action(s) - -```js -// simplest, straight object -const duStuffAction = { type: 'foo', payload: 'bar', baz: 'boz' }; -dispatch(doStuffAction); - -// Methods that return directly an object works almost the same naturally -const doStuff = () = ({ type: 'foo', payload: 'bar', baz: 'boz' }); -dispatch(doStuff()); - -// Also valid -const doStuff = (dispatch, getState) => { - const foo = getFoo(getState()); - dispatch({ type: 'foo', payload: foo, baz: 'boz'}); -} -dispatch(doStuff) - -// Thunk, it is not necessarily async btw -const doStuff = () = async dispatch => { - const baz = await getBoz() - return dispatch({ type: 'foo', payload: 'bar', baz }); -} -await dispatch(doStuff()) - -// real world thunk -const doManyStuffs = () => async (dispatch, getState) => { - try { - const state = getState(); - const userId = getUserId(state); - const isLoggedIn = isConnected(state); - - dispatch({ type: 'any', foo: isConnected ? 'bar' : 'baz' }); - - const anythingResult = await anything(); - fooBar(anythingResult); - - await dispatch({ type: 'thing'}) - return dispatch({ type: 'could', happen: true}); - - } catch(error) { - // handle errors - monitorError(error); - dispatch({ type: 'again?'}); - } -} -dispatch(doManyStuff()) -``` - -## Step by step - -### articles.actions.js - -Create `src/modules/articles/articles.actions.js` - -It should export a `requestArticles` curried function, no parameter on first call and async dispatch on second call. -The body of the function should be an awaited call to the `getArticles` API utils method. -The return statement should call dispatch with an object given properties "type" set to `articles/RECEIVED_ARTICLES` and "articles" set to the resolved promise of `getArticles` call. - -### articles.reducer.js - -Create `src/modules/articles/articles.reducer.js` - -A reducer looks like this: - -```js -import SOME_ACTION_TYPE from './foo.actions'; - -export const initialState = { someMeaningfulKey: null, otherThing: true }; - -export const reducerFoo = (state = initialState, action) => { - switch (action.type) { - case SOME_ACTION_TYPE: - return { - ...state, - someMeaningfulKey: action.payload, - otherThing: action.bar, - }; - default: - return state; - } -}; -``` - -The property you access in **action** are the one you dispatched: - -```js -export const someAction = data => (dispatch, getState) => { - const isBar = isBarSelector(getState()); - return dispatch({ type: SOME_ACTION_TYPE, payload: data, bar: isBar }); -}; -``` - -It should export an **initialState** object - -```js -export const initialState = { - articles: [], -}; -``` - -It should export an **articlesReducer** method - -```js -export const articlesReducer = () => {}; -``` - -Catch the **RECEIVED_ARTICLES** action type and spread the action articles payload in the state. - -### articles.context.js - -Create `src/modules/articles/articles.context.js` -Basically copy [This snippet](https://kentcdodds.com/blog/how-to-use-react-context-effectively#conclusion), our reducer/context implementation is based on it. - -### articles.selectors.js - -Create `src/modules/articles/articles.selectors.js`. In redux a selector is a function taking the state as parameter and returning a value from it, in our case something like: - -```js -const getArticlesSelector = state => state.articles; - -const ArticlesList = () => { - const articles = getArticlesSelector(useArticlesState()); -}; - -// This could be implemented -const ArticlesList = () => { - const articles = useArticlesStateWithSelector(getArticlesSelector); -}; -``` - -But this is not very pretty so lets implement redux useSelector in `src/utils/context.utils.js` at the difference that we need to give it access to state. - -```js -export const useSelector = (useReducerState, selector = state => state) => { - if (!useReducerState) { - throw new Error( - 'You need to provide the reducer State of this resource to get its state and dispatch', - ); - } - - const state = useReducerState(); - - return selector(state); -}; - -// usage -const articles = useSelector(useArticlesState, getArticlesSelector); -``` - -This is great, this is what you would expect from redux, why not. But as e creating custom stateful hooks that we will reuse, why not implement a fetching effect whenever the state doesn't match a condition ? - -```js -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; -}; - -// usage in separate files -export const useArticlesSelector = () => - useSelector(useArticles, ({ articles }) => articles, { - shouldFetch: true, - fetchCondition: articles => articles.length === 0, - fetchAction: requestArticles, - }); - -const articles = useArticlesSelector(); -``` - -### article.component - -We need some basic component to handle displaying the article page from the slug in the url. - -Create `src/modules/articles/components/article.component.js` - -It should Named export `Article` as a function with on prop **id** -It should use your articles selector to get the articles -It should find in the articles the id matching the slug -It should return the article matching the slug or null - -### article.page - -Create `src/pages/article.page.js` - -The page container, use the same format as about/contact page and append the ArticlesList at the end - -### App.js - -Create new route before `/`, you can declare route with parameters this way - -```js - - - -``` - -Wrap the two articles routes in their own Switch while closing the first one on the top. -You should get two siblings Switch. -Wrap the articles' routes' switch in your ArticlesProvider. -You should be able to access your routes by clicking on the cards or directly from their matching url diff --git a/exercises/exercise-5/src/__tests__/App.spec.js b/exercises/exercise-5/src/__tests__/App.spec.js deleted file mode 100644 index 34e1bc5..0000000 --- a/exercises/exercise-5/src/__tests__/App.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -describe('App', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); diff --git a/exercises/exercise-5/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-5/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index 6b23ab8..0000000 --- a/exercises/exercise-5/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App should render correctly 1`] = ` - - - - - - - - - - - - - -`; diff --git a/exercises/exercise-5/src/components/__tests__/__snapshots__/layout.component.spec.js.snap b/exercises/exercise-5/src/components/__tests__/__snapshots__/layout.component.spec.js.snap deleted file mode 100644 index 3de464d..0000000 --- a/exercises/exercise-5/src/components/__tests__/__snapshots__/layout.component.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - -

    - foo -

    -
    -
    -`; diff --git a/exercises/exercise-5/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap b/exercises/exercise-5/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap deleted file mode 100644 index fd43119..0000000 --- a/exercises/exercise-5/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap +++ /dev/null @@ -1,106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    -`; diff --git a/exercises/exercise-5/src/components/__tests__/layout.component.spec.js b/exercises/exercise-5/src/components/__tests__/layout.component.spec.js deleted file mode 100644 index 6add5d5..0000000 --- a/exercises/exercise-5/src/components/__tests__/layout.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Layout } from '../layout.component'; - -let wrapper; - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('NavBar').exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-5/src/components/__tests__/navbar.component.spec.js b/exercises/exercise-5/src/components/__tests__/navbar.component.spec.js deleted file mode 100644 index 3a8f781..0000000 --- a/exercises/exercise-5/src/components/__tests__/navbar.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AppBar, IconButton, Menu, Typography } from '@material-ui/core'; - -import NavBar from '../navbar.component'; - -let wrapper; - -const getWrapper = () => shallow(); - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(AppBar).exists()).toBeTruthy(); - expect(wrapper.find(Typography).text()).toBe('Shopping App'); - }); - }); - - describe('Interaction checks', () => { - it('should sur anchorEl from clicked item', () => { - expect(wrapper.find(Menu).prop('open')).toBeFalsy(); - wrapper.find(IconButton).prop('onClick')({ currentTarget: 'foo' }); - wrapper.update(); - expect(wrapper.find(Menu).prop('open')).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-5/src/components/layout.component.js b/exercises/exercise-5/src/components/layout.component.js deleted file mode 100644 index d8bede0..0000000 --- a/exercises/exercise-5/src/components/layout.component.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-5/src/components/navbar.component.js b/exercises/exercise-5/src/components/navbar.component.js deleted file mode 100644 index a4bcf0d..0000000 --- a/exercises/exercise-5/src/components/navbar.component.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -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'; - -const useStyles = makeStyles(theme => ({ - root: { - flexGrow: 1, - }, - menuButton: { - marginRight: theme.spacing(2), - }, - title: { - flexGrow: 1, - }, -})); - -export default function NavBar() { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - - const handleMenu = event => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    - ); -} diff --git a/exercises/exercise-5/src/constants/proptypes.constants.js b/exercises/exercise-5/src/constants/proptypes.constants.js deleted file mode 100644 index bfe9d9a..0000000 --- a/exercises/exercise-5/src/constants/proptypes.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ - PropTypes.array.isRequired, - PropTypes.object, - PropTypes.element, -]).isRequired; diff --git a/exercises/exercise-5/src/index.css b/exercises/exercise-5/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-5/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-5/src/index.js b/exercises/exercise-5/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-5/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-5/src/logo.svg b/exercises/exercise-5/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-5/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-5/src/modules/articles/__tests__/__snapshots__/articleCard.component.spec.js.snap b/exercises/exercise-5/src/modules/articles/__tests__/__snapshots__/articleCard.component.spec.js.snap deleted file mode 100644 index 0f9fba5..0000000 --- a/exercises/exercise-5/src/modules/articles/__tests__/__snapshots__/articleCard.component.spec.js.snap +++ /dev/null @@ -1,59 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - bar - -
    - - 1 - - - 1 - $ - -
    -
    - - - Lorem - - - Ipsum - - -
    -
    -`; diff --git a/exercises/exercise-5/src/modules/articles/__tests__/__snapshots__/articlesList.component.spec.js.snap b/exercises/exercise-5/src/modules/articles/__tests__/__snapshots__/articlesList.component.spec.js.snap deleted file mode 100644 index b116803..0000000 --- a/exercises/exercise-5/src/modules/articles/__tests__/__snapshots__/articlesList.component.spec.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -`; - -exports[` Snapshot should render list correctly 1`] = ` - -`; diff --git a/exercises/exercise-5/src/modules/articles/__tests__/articleCard.component.spec.js b/exercises/exercise-5/src/modules/articles/__tests__/articleCard.component.spec.js deleted file mode 100644 index 48b3bef..0000000 --- a/exercises/exercise-5/src/modules/articles/__tests__/articleCard.component.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticleCard } from '../articleCard.component'; - -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'; - -let wrapper; - -const article = { - id: 'abc', - slug: 'foo', - name: 'bar', - year: '1', - image: 'baz', - price: 1, -}; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(CardMedia).prop('image')).toBe(article.image); - expect(wrapper.find(CardContent).find(Typography).first().text()).toBe( - article.name, - ); - expect( - wrapper.find(CardContent).find('div').find(Typography).first().text(), - ).toBe(article.year); - expect( - wrapper.find(CardContent).find('div').find(Typography).last().text(), - ).toBe(`${article.price} $`); - expect(wrapper.find(CardActions).find(Button).first().text()).toBe( - 'Lorem', - ); - expect(wrapper.find(CardActions).find(Button).last().text()).toBe( - 'Ipsum', - ); - }); - }); -}); diff --git a/exercises/exercise-5/src/modules/articles/__tests__/articlesList.component.spec.js b/exercises/exercise-5/src/modules/articles/__tests__/articlesList.component.spec.js deleted file mode 100644 index ddf2b5f..0000000 --- a/exercises/exercise-5/src/modules/articles/__tests__/articlesList.component.spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { shallow } from 'enzyme'; - -import { Grid } from '@material-ui/core'; - -import { ArticlesList } from '../articlesList.component'; - -import MockedArticles from '../../../../../fixtures/articles.json'; -import * as ApiUtils from '../../../utils/api.utils'; - -let wrapper; -const emptyArray = []; - -ApiUtils.getArticles = jest.fn().mockResolvedValue(MockedArticles); - -jest.mock('react', () => - global.mockReactWithHooks({ effect: true, state: true }), -); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - useEffect.mockClear(); - useState.mockClear(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should render list correctly', () => { - useState.mockReturnValueOnce([MockedArticles, jest.fn()]); - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).exists()).toBeTruthy(); - }); - }); - - describe('State checks', () => { - // Don't test Jest mocks or React, it is useless - // In the following example we test that our react mock is working - // const [initialsArticles] = useState.mock.results[0].value; - // expect(initialsArticles).toEqual(emptyArray); - - it('should call useState with an empty array', () => { - expect(useState).toHaveBeenCalledWith(emptyArray); - }); - }); - - describe('Effects checks', () => { - it('should call useEffect with a function and an empty array', () => { - expect(useEffect).toHaveBeenCalledWith(expect.any(Function), [ - emptyArray, - ]); - }); - - it('should call getArticles once', () => { - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call getArticles once even when re-rendered', () => { - wrapper.update(); - expect(ApiUtils.getArticles).toHaveBeenCalledTimes(1); - }); - - it('should call setArticles once with MockedArticles', () => { - const [, setArticles] = useState.mock.results[0].value; - expect(setArticles).toHaveBeenCalledWith(MockedArticles); - expect(setArticles).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/exercises/exercise-5/src/modules/articles/articleCard.component.js b/exercises/exercise-5/src/modules/articles/articleCard.component.js deleted file mode 100644 index 36baf94..0000000 --- a/exercises/exercise-5/src/modules/articles/articleCard.component.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -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'; - -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, price } = article; - const classes = useStyles(); - - 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/exercises/exercise-5/src/modules/articles/articlesList.component.js b/exercises/exercise-5/src/modules/articles/articlesList.component.js deleted file mode 100644 index 4e9a695..0000000 --- a/exercises/exercise-5/src/modules/articles/articlesList.component.js +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import Grid from '@material-ui/core/Grid'; - -import { ArticleCard } from './articleCard.component'; -import { getArticles } from '@react-course-v2/api'; - -export function ArticlesList() { - const [articles, setArticles] = useState([]); - - useEffect(() => { - if (articles.length !== 0) { - return; - } - getArticles().then(setArticles).catch(console.error); - }, [articles]); - - return ( - - {articles.map(article => ( - - ))} - - ); -} diff --git a/exercises/exercise-5/src/pages/__tests__/__snapshots__/about.page.spec.js.snap b/exercises/exercise-5/src/pages/__tests__/__snapshots__/about.page.spec.js.snap deleted file mode 100644 index 71bc0e1..0000000 --- a/exercises/exercise-5/src/pages/__tests__/__snapshots__/about.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - About -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-5/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap b/exercises/exercise-5/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap deleted file mode 100644 index 8c0408c..0000000 --- a/exercises/exercise-5/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - Contact -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-5/src/pages/__tests__/__snapshots__/home.page.spec.js.snap b/exercises/exercise-5/src/pages/__tests__/__snapshots__/home.page.spec.js.snap deleted file mode 100644 index 182e889..0000000 --- a/exercises/exercise-5/src/pages/__tests__/__snapshots__/home.page.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -

    - Home Page -

    - -
    -`; diff --git a/exercises/exercise-5/src/pages/__tests__/about.page.spec.js b/exercises/exercise-5/src/pages/__tests__/about.page.spec.js deleted file mode 100644 index 6d7eb91..0000000 --- a/exercises/exercise-5/src/pages/__tests__/about.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AboutPage } from '../about.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-5/src/pages/__tests__/contact.page.spec.js b/exercises/exercise-5/src/pages/__tests__/contact.page.spec.js deleted file mode 100644 index 0e29df1..0000000 --- a/exercises/exercise-5/src/pages/__tests__/contact.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ContactPage } from '../contact.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-5/src/pages/__tests__/home.page.spec.js b/exercises/exercise-5/src/pages/__tests__/home.page.spec.js deleted file mode 100644 index 11eb6d9..0000000 --- a/exercises/exercise-5/src/pages/__tests__/home.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { HomePage } from '../home.page'; - -let wrapper; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect(wrapper.find(`ArticlesList`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-5/src/pages/about.page.js b/exercises/exercise-5/src/pages/about.page.js deleted file mode 100644 index 567e4b9..0000000 --- a/exercises/exercise-5/src/pages/about.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-5/src/pages/contact.page.js b/exercises/exercise-5/src/pages/contact.page.js deleted file mode 100644 index 1e2e74b..0000000 --- a/exercises/exercise-5/src/pages/contact.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-5/src/pages/home.page.js b/exercises/exercise-5/src/pages/home.page.js deleted file mode 100644 index f8cb6b7..0000000 --- a/exercises/exercise-5/src/pages/home.page.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -import { Layout } from '../components/layout.component'; -import { ArticlesList } from '../modules/articles/articlesList.component'; - -export const HomePage = () => { - return ( - -

    Home Page

    - -
    - ); -}; diff --git a/exercises/exercise-6/README.md b/exercises/exercise-6/README.md deleted file mode 100644 index d493ef0..0000000 --- a/exercises/exercise-6/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# 6/ Sharing state between providers and components - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | ---------------------------------------------------------- | -| Create | src/modules/cart/cart.actions.js | {addToCart, removeFromCart, ADD_TO_CART, REMOVE_FROM_CART} | -| Create | src/modules/cart/cart.reducer.js | {initialState, cartReducer} | -| Create | src/modules/cart/cart.context.js | {useCart, useCartState, useCartDispatch, CartProvider} | -| Create | src/modules/cart/components/cart.component.js | {Cart} | -| Create | src/modules/cart/components/cartLayout.component.js | {CartLayout} | -| Modify | src/App.js | {App} | -| Modify | src/pages/home.page.js | {HomePage} | -| Modify | src/modules/articles/components/articleCard.component.js | {ArticleCard} | - -## TL;DR - -Let's create the Cart module ! - -We need a Shopping Cart that is fixed and stays with us along our navigation through our incredibly fascinating Shopping App. - -Let's think of it as HTML first, it's like the aside of a section. - -Where should it belong ? It's a module, of course, but it should be instantiated at a page level, and it kinda bring the need to add some wrapper, flex and box sizing to every pages. -We have two options: - -- Create a **CartLayout** components as a parent for the pages content, just like the page wrapper **Layout** -- Create a HOC (Higher order Component), to add the said Layout. - -The two solutions are very similar but, in our case, the only thing we need is some reusable jsx across pages, it's a regular Component role to needs, rather than adding logic or interact with props, which would fit more with a HOC. - -## Step by step - -To begin with, let's duplicate the **articles** modules and rename it cart, it should be pretty step forward to adapt it. - -### src/modules/cart/cart.actions.js - -Remove everything from the past articles. -Create two action creators **ADD_TO_CART** and **REMOVE_FROM_CART** -Create two simple methods that returns straight object actions. **addToCart** takes `article` as only parameter and returns `article` as property in the action. -**removeFromCart** takes `article` as only parameter and returns id as property in the action - -### src/modules/cart/cart.reducer.js - -A Shopping Cart is nothing too ambitious to build but you need at least to let users increment and decrement the shopping items they added to the cart. The reducer is in charge of those calculations. -You could be tempted to think of it as a list, an array you'd map around to display some `
  • `. You could push and slice, why not. -But, while this is possible, this would demand a lot of calculations at each update and won't be very readable. - -A _Map_, or in our case an object with articles ids as keys suits better our needs, we can increment and decrement the articles in the cart by attaching a property `occurrences` to them in the state. - -Create and export the initial state `{ articles: {}}` -Catch the two actions created in cart.actions - -On ADD_TO_CART: - -- Check if there is an **id** corresponding to the **action.article** one, if so, return early by setting the **action.article** in the articles, using **action.id** as key. -- Afterwards, it means we have found an existing similar article in the state, so we only need to get its number of occurrences in the state. As we haven't set it on the first article we got in the early return, it means the value of `state.articles[article.id].occurrences` is _undefined_ when you are adding a second occurrence. Only modify the number of occurrences of the matching article in the state. - -On REMOVE_FROM_LIST: - -- Start by checking if the property **occurrences** of the `state.articles[action.id]` is set to a number superior to 1. If so return the state with this given article occurrences decremented by one. -- Otherwise, the value **occurrences** is either nil or 1, meaning this time you need to completely delete the matching article entry from the state instead of decrementing its **occurrences**. Return the state with the remaining articles. diff --git a/exercises/exercise-6/src/App.js b/exercises/exercise-6/src/App.js deleted file mode 100644 index 8544816..0000000 --- a/exercises/exercise-6/src/App.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Switch, Route, BrowserRouter as Router } from 'react-router-dom'; - -import { ArticlesProvider } from './modules/articles/articles.context'; - -import { HomePage } from './pages/home.page'; -import { ArticlePage } from './pages/article.page'; -import { AboutPage } from './pages/about.page'; -import { ContactPage } from './pages/contact.page'; - -export default function App() { - return ( - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/exercises/exercise-6/src/__hints__/dialog.js b/exercises/exercise-6/src/__hints__/dialog.js deleted file mode 100644 index 079a0bc..0000000 --- a/exercises/exercise-6/src/__hints__/dialog.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import instructionsMd from './instructions.md'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; -import emoji from 'emoji-dictionary'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - ['& blockquote']: { - boxShadow: - '0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%)', - borderRadius: '4px', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - padding: '20px', - borderLeft: '5px solid #3f51b5', - }, - - ['& p']: { - lineHeight: '25px', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-6/src/__hints__/instructions.md b/exercises/exercise-6/src/__hints__/instructions.md deleted file mode 100644 index d493ef0..0000000 --- a/exercises/exercise-6/src/__hints__/instructions.md +++ /dev/null @@ -1,60 +0,0 @@ -# 6/ Sharing state between providers and components - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | ---------------------------------------------------------- | -| Create | src/modules/cart/cart.actions.js | {addToCart, removeFromCart, ADD_TO_CART, REMOVE_FROM_CART} | -| Create | src/modules/cart/cart.reducer.js | {initialState, cartReducer} | -| Create | src/modules/cart/cart.context.js | {useCart, useCartState, useCartDispatch, CartProvider} | -| Create | src/modules/cart/components/cart.component.js | {Cart} | -| Create | src/modules/cart/components/cartLayout.component.js | {CartLayout} | -| Modify | src/App.js | {App} | -| Modify | src/pages/home.page.js | {HomePage} | -| Modify | src/modules/articles/components/articleCard.component.js | {ArticleCard} | - -## TL;DR - -Let's create the Cart module ! - -We need a Shopping Cart that is fixed and stays with us along our navigation through our incredibly fascinating Shopping App. - -Let's think of it as HTML first, it's like the aside of a section. - -Where should it belong ? It's a module, of course, but it should be instantiated at a page level, and it kinda bring the need to add some wrapper, flex and box sizing to every pages. -We have two options: - -- Create a **CartLayout** components as a parent for the pages content, just like the page wrapper **Layout** -- Create a HOC (Higher order Component), to add the said Layout. - -The two solutions are very similar but, in our case, the only thing we need is some reusable jsx across pages, it's a regular Component role to needs, rather than adding logic or interact with props, which would fit more with a HOC. - -## Step by step - -To begin with, let's duplicate the **articles** modules and rename it cart, it should be pretty step forward to adapt it. - -### src/modules/cart/cart.actions.js - -Remove everything from the past articles. -Create two action creators **ADD_TO_CART** and **REMOVE_FROM_CART** -Create two simple methods that returns straight object actions. **addToCart** takes `article` as only parameter and returns `article` as property in the action. -**removeFromCart** takes `article` as only parameter and returns id as property in the action - -### src/modules/cart/cart.reducer.js - -A Shopping Cart is nothing too ambitious to build but you need at least to let users increment and decrement the shopping items they added to the cart. The reducer is in charge of those calculations. -You could be tempted to think of it as a list, an array you'd map around to display some `
  • `. You could push and slice, why not. -But, while this is possible, this would demand a lot of calculations at each update and won't be very readable. - -A _Map_, or in our case an object with articles ids as keys suits better our needs, we can increment and decrement the articles in the cart by attaching a property `occurrences` to them in the state. - -Create and export the initial state `{ articles: {}}` -Catch the two actions created in cart.actions - -On ADD_TO_CART: - -- Check if there is an **id** corresponding to the **action.article** one, if so, return early by setting the **action.article** in the articles, using **action.id** as key. -- Afterwards, it means we have found an existing similar article in the state, so we only need to get its number of occurrences in the state. As we haven't set it on the first article we got in the early return, it means the value of `state.articles[article.id].occurrences` is _undefined_ when you are adding a second occurrence. Only modify the number of occurrences of the matching article in the state. - -On REMOVE_FROM_LIST: - -- Start by checking if the property **occurrences** of the `state.articles[action.id]` is set to a number superior to 1. If so return the state with this given article occurrences decremented by one. -- Otherwise, the value **occurrences** is either nil or 1, meaning this time you need to completely delete the matching article entry from the state instead of decrementing its **occurrences**. Return the state with the remaining articles. diff --git a/exercises/exercise-6/src/__tests__/App.spec.js b/exercises/exercise-6/src/__tests__/App.spec.js deleted file mode 100644 index 34e1bc5..0000000 --- a/exercises/exercise-6/src/__tests__/App.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -describe('App', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); diff --git a/exercises/exercise-6/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-6/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index 19b610d..0000000 --- a/exercises/exercise-6/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App should render correctly 1`] = ` - - - - - - - - - - - - - - - - - - - - -`; diff --git a/exercises/exercise-6/src/components/__tests__/__snapshots__/layout.component.spec.js.snap b/exercises/exercise-6/src/components/__tests__/__snapshots__/layout.component.spec.js.snap deleted file mode 100644 index 3de464d..0000000 --- a/exercises/exercise-6/src/components/__tests__/__snapshots__/layout.component.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - -

    - foo -

    -
    -
    -`; diff --git a/exercises/exercise-6/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap b/exercises/exercise-6/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap deleted file mode 100644 index fd43119..0000000 --- a/exercises/exercise-6/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap +++ /dev/null @@ -1,106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    -`; diff --git a/exercises/exercise-6/src/components/__tests__/layout.component.spec.js b/exercises/exercise-6/src/components/__tests__/layout.component.spec.js deleted file mode 100644 index 6add5d5..0000000 --- a/exercises/exercise-6/src/components/__tests__/layout.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Layout } from '../layout.component'; - -let wrapper; - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('NavBar').exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-6/src/components/__tests__/navbar.component.spec.js b/exercises/exercise-6/src/components/__tests__/navbar.component.spec.js deleted file mode 100644 index 3a8f781..0000000 --- a/exercises/exercise-6/src/components/__tests__/navbar.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AppBar, IconButton, Menu, Typography } from '@material-ui/core'; - -import NavBar from '../navbar.component'; - -let wrapper; - -const getWrapper = () => shallow(); - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(AppBar).exists()).toBeTruthy(); - expect(wrapper.find(Typography).text()).toBe('Shopping App'); - }); - }); - - describe('Interaction checks', () => { - it('should sur anchorEl from clicked item', () => { - expect(wrapper.find(Menu).prop('open')).toBeFalsy(); - wrapper.find(IconButton).prop('onClick')({ currentTarget: 'foo' }); - wrapper.update(); - expect(wrapper.find(Menu).prop('open')).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-6/src/components/layout.component.js b/exercises/exercise-6/src/components/layout.component.js deleted file mode 100644 index d8bede0..0000000 --- a/exercises/exercise-6/src/components/layout.component.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-6/src/components/navbar.component.js b/exercises/exercise-6/src/components/navbar.component.js deleted file mode 100644 index a4bcf0d..0000000 --- a/exercises/exercise-6/src/components/navbar.component.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -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'; - -const useStyles = makeStyles(theme => ({ - root: { - flexGrow: 1, - }, - menuButton: { - marginRight: theme.spacing(2), - }, - title: { - flexGrow: 1, - }, -})); - -export default function NavBar() { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - - const handleMenu = event => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    - ); -} diff --git a/exercises/exercise-6/src/constants/proptypes.constants.js b/exercises/exercise-6/src/constants/proptypes.constants.js deleted file mode 100644 index bfe9d9a..0000000 --- a/exercises/exercise-6/src/constants/proptypes.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ - PropTypes.array.isRequired, - PropTypes.object, - PropTypes.element, -]).isRequired; diff --git a/exercises/exercise-6/src/index.css b/exercises/exercise-6/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-6/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-6/src/index.js b/exercises/exercise-6/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-6/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-6/src/logo.svg b/exercises/exercise-6/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-6/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-6/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap b/exercises/exercise-6/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap deleted file mode 100644 index 5a12ea9..0000000 --- a/exercises/exercise-6/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`articles.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-6/src/modules/articles/__tests__/articles.actions.spec.js b/exercises/exercise-6/src/modules/articles/__tests__/articles.actions.spec.js deleted file mode 100644 index 3c97a72..0000000 --- a/exercises/exercise-6/src/modules/articles/__tests__/articles.actions.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import { RECEIVED_ARTICLES, requestArticles } from '../articles.actions'; - -jest.mock('../../../utils/api.utils', () => ({ - 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/exercises/exercise-6/src/modules/articles/__tests__/articles.reducer.spec.js b/exercises/exercise-6/src/modules/articles/__tests__/articles.reducer.spec.js deleted file mode 100644 index 4a9dfc1..0000000 --- a/exercises/exercise-6/src/modules/articles/__tests__/articles.reducer.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -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/exercises/exercise-6/src/modules/articles/articles.actions.js b/exercises/exercise-6/src/modules/articles/articles.actions.js deleted file mode 100644 index f4cc0e3..0000000 --- a/exercises/exercise-6/src/modules/articles/articles.actions.js +++ /dev/null @@ -1,9 +0,0 @@ -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/exercises/exercise-6/src/modules/articles/articles.context.js b/exercises/exercise-6/src/modules/articles/articles.context.js deleted file mode 100644 index 3f16199..0000000 --- a/exercises/exercise-6/src/modules/articles/articles.context.js +++ /dev/null @@ -1,52 +0,0 @@ -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/exercises/exercise-6/src/modules/articles/articles.reducer.js b/exercises/exercise-6/src/modules/articles/articles.reducer.js deleted file mode 100644 index b6518e5..0000000 --- a/exercises/exercise-6/src/modules/articles/articles.reducer.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-6/src/modules/articles/articles.selectors.js b/exercises/exercise-6/src/modules/articles/articles.selectors.js deleted file mode 100644 index a5ec396..0000000 --- a/exercises/exercise-6/src/modules/articles/articles.selectors.js +++ /dev/null @@ -1,10 +0,0 @@ -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/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap b/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap deleted file mode 100644 index c3be7f5..0000000 --- a/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`
    Snapshot should render correctly 1`] = ` - -`; diff --git a/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap b/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap deleted file mode 100644 index 253f35e..0000000 --- a/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - bar - -
    - - 1 - - - 1 - $ - -
    -
    - - - Lorem - - - See more - - -
    -
    -`; diff --git a/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap b/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap deleted file mode 100644 index 09117d2..0000000 --- a/exercises/exercise-6/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - - - - -`; diff --git a/exercises/exercise-6/src/modules/articles/components/__tests__/article.component.spec.js b/exercises/exercise-6/src/modules/articles/components/__tests__/article.component.spec.js deleted file mode 100644 index 3c381e8..0000000 --- a/exercises/exercise-6/src/modules/articles/components/__tests__/article.component.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Article } from '../article.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = id => shallow(
    ); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('
    ', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(getWrapper(MockedArticles[0].slug)).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect( - getWrapper(MockedArticles[0].slug).find('ArticleCard').prop('article'), - ).toMatchObject(MockedArticles[0]); - }); - - it('should be null when no id is passed', () => { - expect(getWrapper('foo')).toBeEmptyRender(); - }); - }); -}); diff --git a/exercises/exercise-6/src/modules/articles/components/__tests__/articleCard.component.spec.js b/exercises/exercise-6/src/modules/articles/components/__tests__/articleCard.component.spec.js deleted file mode 100644 index 9475173..0000000 --- a/exercises/exercise-6/src/modules/articles/components/__tests__/articleCard.component.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticleCard } from '../articleCard.component'; - -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'; - -let wrapper; - -const article = { - id: 'abc', - slug: 'foo', - name: 'bar', - year: '1', - image: 'baz', - price: 1, -}; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(CardMedia).prop('image')).toBe(article.image); - expect(wrapper.find(CardContent).find(Typography).first().text()).toBe( - article.name, - ); - expect( - wrapper.find(CardContent).find('div').find(Typography).first().text(), - ).toBe(article.year); - expect( - wrapper.find(CardContent).find('div').find(Typography).last().text(), - ).toBe(`${article.price} $`); - expect(wrapper.find(CardActions).find(Button).first().text()).toBe( - 'Lorem', - ); - expect(wrapper.find(CardActions).find(Button).last().text()).toBe( - 'See more', - ); - }); - }); -}); diff --git a/exercises/exercise-6/src/modules/articles/components/__tests__/articlesList.component.spec.js b/exercises/exercise-6/src/modules/articles/components/__tests__/articlesList.component.spec.js deleted file mode 100644 index 1b62497..0000000 --- a/exercises/exercise-6/src/modules/articles/components/__tests__/articlesList.component.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Grid } from '@material-ui/core'; - -import { ArticlesList } from '../articlesList.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -let wrapper; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(Grid) - .children() - .map(node => node.prop('article')), - ).toMatchObject(MockedArticles); - }); - }); -}); diff --git a/exercises/exercise-6/src/modules/articles/components/article.component.js b/exercises/exercise-6/src/modules/articles/components/article.component.js deleted file mode 100644 index 4ac07c4..0000000 --- a/exercises/exercise-6/src/modules/articles/components/article.component.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-6/src/modules/articles/components/articleCard.component.js b/exercises/exercise-6/src/modules/articles/components/articleCard.component.js deleted file mode 100644 index 75ce55e..0000000 --- a/exercises/exercise-6/src/modules/articles/components/articleCard.component.js +++ /dev/null @@ -1,83 +0,0 @@ -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'; - -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(); - - 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/exercises/exercise-6/src/modules/articles/components/articlesList.component.js b/exercises/exercise-6/src/modules/articles/components/articlesList.component.js deleted file mode 100644 index 5fa4989..0000000 --- a/exercises/exercise-6/src/modules/articles/components/articlesList.component.js +++ /dev/null @@ -1,19 +0,0 @@ -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/exercises/exercise-6/src/pages/__tests__/__snapshots__/about.page.spec.js.snap b/exercises/exercise-6/src/pages/__tests__/__snapshots__/about.page.spec.js.snap deleted file mode 100644 index 71bc0e1..0000000 --- a/exercises/exercise-6/src/pages/__tests__/__snapshots__/about.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - About -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-6/src/pages/__tests__/__snapshots__/article.page.spec.js.snap b/exercises/exercise-6/src/pages/__tests__/__snapshots__/article.page.spec.js.snap deleted file mode 100644 index 72f21ac..0000000 --- a/exercises/exercise-6/src/pages/__tests__/__snapshots__/article.page.spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - -

    - Article - foo -

    - - Return to Home - -
    -
    - -`; diff --git a/exercises/exercise-6/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap b/exercises/exercise-6/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap deleted file mode 100644 index 8c0408c..0000000 --- a/exercises/exercise-6/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - Contact -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-6/src/pages/__tests__/__snapshots__/home.page.spec.js.snap b/exercises/exercise-6/src/pages/__tests__/__snapshots__/home.page.spec.js.snap deleted file mode 100644 index 182e889..0000000 --- a/exercises/exercise-6/src/pages/__tests__/__snapshots__/home.page.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -

    - Home Page -

    - -
    -`; diff --git a/exercises/exercise-6/src/pages/__tests__/about.page.spec.js b/exercises/exercise-6/src/pages/__tests__/about.page.spec.js deleted file mode 100644 index 6d7eb91..0000000 --- a/exercises/exercise-6/src/pages/__tests__/about.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AboutPage } from '../about.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-6/src/pages/__tests__/article.page.spec.js b/exercises/exercise-6/src/pages/__tests__/article.page.spec.js deleted file mode 100644 index a32b00b..0000000 --- a/exercises/exercise-6/src/pages/__tests__/article.page.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticlePage } from '../article.page'; - -let wrapper; - -jest.mock('react-router-dom', () => ({ - useParams: jest.fn().mockReturnValue({ id: 'foo' }), -})); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`Article`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-6/src/pages/__tests__/contact.page.spec.js b/exercises/exercise-6/src/pages/__tests__/contact.page.spec.js deleted file mode 100644 index 0e29df1..0000000 --- a/exercises/exercise-6/src/pages/__tests__/contact.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ContactPage } from '../contact.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-6/src/pages/__tests__/home.page.spec.js b/exercises/exercise-6/src/pages/__tests__/home.page.spec.js deleted file mode 100644 index 11eb6d9..0000000 --- a/exercises/exercise-6/src/pages/__tests__/home.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { HomePage } from '../home.page'; - -let wrapper; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect(wrapper.find(`ArticlesList`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-6/src/pages/about.page.js b/exercises/exercise-6/src/pages/about.page.js deleted file mode 100644 index 567e4b9..0000000 --- a/exercises/exercise-6/src/pages/about.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-6/src/pages/article.page.js b/exercises/exercise-6/src/pages/article.page.js deleted file mode 100644 index 2d17814..0000000 --- a/exercises/exercise-6/src/pages/article.page.js +++ /dev/null @@ -1,24 +0,0 @@ -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'; - -export const ArticlePage = () => { - const { id } = useParams(); - - return ( - - -

    Article {id}

    - -
    -
    - - ); -}; diff --git a/exercises/exercise-6/src/pages/contact.page.js b/exercises/exercise-6/src/pages/contact.page.js deleted file mode 100644 index 1e2e74b..0000000 --- a/exercises/exercise-6/src/pages/contact.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-6/src/pages/home.page.js b/exercises/exercise-6/src/pages/home.page.js deleted file mode 100644 index 9566ada..0000000 --- a/exercises/exercise-6/src/pages/home.page.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; - -import { Layout } from '../components/layout.component'; -import { ArticlesList } from '../modules/articles/components/articlesList.component'; - -export const HomePage = () => { - return ( - -

    Home Page

    - -
    - ); -}; diff --git a/exercises/exercise-6/src/utils/context.utils.js b/exercises/exercise-6/src/utils/context.utils.js deleted file mode 100644 index 28a4b57..0000000 --- a/exercises/exercise-6/src/utils/context.utils.js +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect } from 'react'; - -export const dispatchThunk = dispatch => param => { - if (typeof param === 'function') { - return param(dispatch); - } - - 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/exercises/exercise-7/README.md b/exercises/exercise-7/README.md deleted file mode 100644 index 709285d..0000000 --- a/exercises/exercise-7/README.md +++ /dev/null @@ -1,163 +0,0 @@ -# 7/ Providers cold shower, a global state struggle - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | ------------------------------------------------------ | -| CREATE | src/modules/user/user.actions.js | {login, logout} | -| CREATE | src/modules/user/user.reducer.js | {initialState, userReducer} | -| CREATE | src/modules/user/user.context.js | {useUser, useUserState, useUserDispatch, UserProvider} | -| CREATE | src/modules/user/user.hook.js | {usePersistedUser} | -| CREATE | src/modules/user/user.selectors.js | {getUser, isConnectedUser} | -| CREATE | src/modules/user/components/login.component.js | {Login} | -| CREATE | src/pages/login.page.js | {LoginPage} | -| CREATE | src/pages/checkout.page.js | {CheckoutPage} | -| CREATE | src/modules/routing/routing.hooks.js | {useLoginRedirect} | -| CREATE | src/modules/routing/routing.constants.js | {ROUTES_PATHS_BY_NAMES, PROTECTED_PATHS} | -| CREATE | src/modules/routing/components/routes.component.js | {AppRoutes} | -| MODIFY | src/App.js | {App} | -| MODIFY | src/modules/articles/components/articleCard.component.js | {ArticleCard} | - -## TL;DR - -It's raining modules, let's create the user, checkout and routing ones ! - -We need a Checkout page but the user must be logged in to access it. So we also need a Login page. -We'll mock the user api and authentication process for now. -We need to be redirected to the login page but only from the checkout page. - -## Step by step - -To begin with, let's duplicate the **articles** modules and rename it user, it should be pretty step forward to adapt it. - -### src/modules/user/user.actions.js - -#### login - -We need a **login** action creator, a thunk in this case. - -```js -/** - * - * @param {string} email - * @param {string} password - */ -export const login = (email, password) => async dispatch => {}; -``` - -You need to: - -- await a call from **signIn** API method with **email** and **password** as parameters to get the user value. -- Set the user in the localStorage under the key "user" -- return dispatch the **LOGIN** action type - -#### logout - -```js -export const logout = () => async (dispatch, getState) => {}; -``` - -You need to: - -- get the user value from the store using the **getUser** selector and **getState** -- call the **signOut** methods othe the API -- Remove the user from the localStorage -- return dispatch the **LOGOUT** action type with user as property - -### src/modules/user/user.reducer.js - -Super dummy reducer ! - -#### LOGIN - -Catch the action **LOGIN** in a case (switch) and set user in the state from the **action.user** property - -#### LOGOUT - -Catch the action in a case (switch) **LOGOUT** and set user in the state to **null** - -### src/modules/user/user.hooks.js - -#### usePersistedUser - -Dummy hook ! - -Returns the **user** from _localStorage_ - -### src/modules/user/user.selectors.js - -#### isUserConnected - -Takes the user context state as parameter and returns a boolean wether it is truthy or not. - -#### getUser - -Takes the user context state as parameter and returns the user entry from it. - -### src/hooks/useInput.hooks.js - -#### useInput - -Create a stateful hook that returns the state value (input) and an **onChange** handler. -Basically you would use a control input this way - -```js -export const MyForm = () => { - // input type text expects a string - const [inputValue, setInputValue] = useState(''); - - const onChange = event => setInputValue(event.target.value); - ... -}; -``` - -### src/modules/user/components/login.component.js - -#### Login - -- Get dispatch from user context -- Integrate two **useInput** to control _email_ and _password_ TextFields -- Create a submit handler for the form that dispatch the **login action** with the **email** and **password** - -### src/pages/login.page.js - -#### LoginPage - -- Create a classic page and add the **Login** component under the habitual layout. - -### src/modules/routing/routing.constants.js - -Let's do a clean enum for our routes and for our protected routes. - -#### ROUTES_PATHS_BY_NAMES - -Create an enum of _routePathsByNames_ - -:warning: The routes exhaustiveness is tested ! - -```js -export const ROUTES_PATHS_BY_NAMES = { - foo: '/foo', -}; -``` - -#### PROTECTED_PATHS - -Create an enum of _routePathsByNames_ - -:warning: The routes exhaustiveness is tested ! - -```js -export const PROTECTED_PATHS = [ROUTES_PATHS_BY_NAMES.checkout]; -``` - -### src/modules/routing/routing.hooks.js - -#### useLoginRedirect - -The main course of this exercise ! -You need to figure out how to redirect the user whenever he is not logged in when he tries to access the checkout page. - -You'll need to use `react-router-dom` **useLocation** for `pathname` checks and **useHistory** to get the `push` method. - -This hook will only perform side effects, it will not return any value. It will use state and useEffects. - -Good luck. diff --git a/exercises/exercise-7/src/App.js b/exercises/exercise-7/src/App.js deleted file mode 100644 index 08f5e11..0000000 --- a/exercises/exercise-7/src/App.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Switch, Route, BrowserRouter as Router } from 'react-router-dom'; - -import { CartProvider } from './modules/cart/cart.context'; -import { ArticlesProvider } from './modules/articles/articles.context'; - -import { HomePage } from './pages/home.page'; -import { ArticlePage } from './pages/article.page'; -import { AboutPage } from './pages/about.page'; -import { ContactPage } from './pages/contact.page'; - -export default function App() { - return ( - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/exercises/exercise-7/src/__hints__/dialog.js b/exercises/exercise-7/src/__hints__/dialog.js deleted file mode 100644 index 9d7643b..0000000 --- a/exercises/exercise-7/src/__hints__/dialog.js +++ /dev/null @@ -1,161 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; - -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; - -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import emoji from 'emoji-dictionary'; - -import instructionsMd from './instructions.md'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - ['& blockquote']: { - boxShadow: - '0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%)', - borderRadius: '4px', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - padding: '20px', - borderLeft: '5px solid #3f51b5', - }, - - ['& p']: { - lineHeight: '25px', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-7/src/__hints__/instructions.md b/exercises/exercise-7/src/__hints__/instructions.md deleted file mode 100644 index ae37c79..0000000 --- a/exercises/exercise-7/src/__hints__/instructions.md +++ /dev/null @@ -1,163 +0,0 @@ -# 7/ Providers cold shower, a global state struggle - -:warning: :man_shrugging: - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | ------------------------------------------------------ | -| CREATE | src/modules/user/user.actions.js | {login, logout} | -| CREATE | src/modules/user/user.reducer.js | {initialState, userReducer} | -| CREATE | src/modules/user/user.context.js | {useUser, useUserState, useUserDispatch, UserProvider} | -| CREATE | src/modules/user/user.hook.js | {usePersistedUser} | -| CREATE | src/modules/user/user.selectors.js | {getUser, isConnectedUser} | -| CREATE | src/modules/user/components/login.component.js | {Login} | -| CREATE | src/pages/login.page.js | {LoginPage} | -| CREATE | src/pages/checkout.page.js | {CheckoutPage} | -| CREATE | src/modules/routing/routing.hooks.js | {useLoginRedirect} | -| CREATE | src/modules/routing/routing.constants.js | {ROUTES_PATHS_BY_NAMES, PROTECTED_PATHS} | -| CREATE | src/modules/routing/components/routes.component.js | {AppRoutes} | -| MODIFY | src/App.js | {App} | -| MODIFY | src/modules/articles/components/articleCard.component.js | {ArticleCard} | - -## TL;DR - -It's raining modules, let's create the user, checkout and routing ones ! - -We need a Checkout page but the user must be logged in to access it. So we also need a Login page. -We'll mock the user api and authentication process for now. -We need to be redirected to the login page but only from the checkout page. - -## Step by step - -To begin with, let's duplicate the **articles** modules and rename it user, it should be pretty step forward to adapt it. - -### src/modules/user/user.actions.js - -#### login - -We need a **login** action creator, a thunk in this case. - -```js -/** - * - * @param {string} email - * @param {string} password - */ -export const login = (email, password) => async dispatch => {}; -``` - -You need to: - -- await a call from **signIn** API method with **email** and **password** as parameters to get the user value. -- Set the user in the localStorage under the key "user" -- return dispatch the **LOGIN** action - -#### logout - -```js -export const logout = () => async (dispatch, getState) => {}; -``` - -You need to: - -- get the user value from the store using the **getUser** selector and **getState** -- call the **signOut** methods othe the API -- Remove the user from the localStorage -- return dispatch the **LOGOUT** action with user as property - -### src/modules/user/user.reducer.js - -Super dummy reducer ! - -#### LOGIN - -Catch the action **LOGIN** and set user in the state from the **action.user** property - -#### LOGOUT - -Catch the action **LOGOUT** and set user in the state to **null** - -### src/modules/user/user.hooks.js - -#### usePersistedUser - -Dummy hook ! - -Returns the **user** from _localStorage_ - -### src/modules/user/user.selectors.js - -#### isUserConnected - -Takes the user context state as parameter and returns a boolean wether it is truthy or not. - -#### getUser - -Takes the user context state as parameter and returns the user entry from it. - -### src/hooks/useInput.hooks.js - -#### useInput - -Create a stateful hook that returns the state value (input) and an **onChange** handler. -Basically you would use a control input this way - -```js -export const MyInput = () => { - // input type text expects a string - const [value, onChange] = useInput(''); - ... -}; -``` - -### src/modules/user/components/login.component.js - -#### Login - -- Get dispatch from user context -- Integrate two **useInput** to control _email_ and _password_ TextFields -- Create a submit handler for the form that dispatch the **login action** with the **email** and **password** - -### src/pages/login.page.js - -#### LoginPage - -- Create a classic page and add the **Login** component under the habitual layout. - -### src/modules/routing/routing.constants.js - -Let's do a clean enum for our routes and for our protected routes. - -#### ROUTES_PATHS_BY_NAMES - -Create an enum of _routePathsByNames_ - -:warning: The routes exhaustiveness is tested ! - -```js -export const ROUTES_PATHS_BY_NAMES = { - foo: '/foo', -}; -``` - -#### PROTECTED_PATHS - -Create an enum of _routePathsByNames_ - -:warning: The routes exhaustiveness is tested ! - -```js -export const PROTECTED_PATHS = [ROUTES_PATHS_BY_NAMES.checkout]; -``` - -### src/modules/routing/routing.hooks.js - -#### useLoginRedirect - -The main course of this exercise ! -You need to figure out how to redirect the user whenever he is not logged in when he tries to access the checkout page. - -You'll need to use `react-router-dom` **useLocation** for `pathname` checks and **useHistory** to get the `push` method. - -This hook will only perform side effects, it will not return any value. It will use state and useEffects. - -Good luck. diff --git a/exercises/exercise-7/src/__tests__/App.spec.js b/exercises/exercise-7/src/__tests__/App.spec.js deleted file mode 100644 index 34e1bc5..0000000 --- a/exercises/exercise-7/src/__tests__/App.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -describe('App', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); diff --git a/exercises/exercise-7/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-7/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index c0893ec..0000000 --- a/exercises/exercise-7/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App should render correctly 1`] = ` - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/exercises/exercise-7/src/components/__tests__/__snapshots__/layout.component.spec.js.snap b/exercises/exercise-7/src/components/__tests__/__snapshots__/layout.component.spec.js.snap deleted file mode 100644 index 3de464d..0000000 --- a/exercises/exercise-7/src/components/__tests__/__snapshots__/layout.component.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - -

    - foo -

    -
    -
    -`; diff --git a/exercises/exercise-7/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap b/exercises/exercise-7/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap deleted file mode 100644 index fd43119..0000000 --- a/exercises/exercise-7/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap +++ /dev/null @@ -1,106 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    -`; diff --git a/exercises/exercise-7/src/components/__tests__/layout.component.spec.js b/exercises/exercise-7/src/components/__tests__/layout.component.spec.js deleted file mode 100644 index 6add5d5..0000000 --- a/exercises/exercise-7/src/components/__tests__/layout.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Layout } from '../layout.component'; - -let wrapper; - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('NavBar').exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-7/src/components/__tests__/navbar.component.spec.js b/exercises/exercise-7/src/components/__tests__/navbar.component.spec.js deleted file mode 100644 index 3a8f781..0000000 --- a/exercises/exercise-7/src/components/__tests__/navbar.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AppBar, IconButton, Menu, Typography } from '@material-ui/core'; - -import NavBar from '../navbar.component'; - -let wrapper; - -const getWrapper = () => shallow(); - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(AppBar).exists()).toBeTruthy(); - expect(wrapper.find(Typography).text()).toBe('Shopping App'); - }); - }); - - describe('Interaction checks', () => { - it('should sur anchorEl from clicked item', () => { - expect(wrapper.find(Menu).prop('open')).toBeFalsy(); - wrapper.find(IconButton).prop('onClick')({ currentTarget: 'foo' }); - wrapper.update(); - expect(wrapper.find(Menu).prop('open')).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-7/src/components/layout.component.js b/exercises/exercise-7/src/components/layout.component.js deleted file mode 100644 index d8bede0..0000000 --- a/exercises/exercise-7/src/components/layout.component.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-7/src/components/navbar.component.js b/exercises/exercise-7/src/components/navbar.component.js deleted file mode 100644 index a4bcf0d..0000000 --- a/exercises/exercise-7/src/components/navbar.component.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -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'; - -const useStyles = makeStyles(theme => ({ - root: { - flexGrow: 1, - }, - menuButton: { - marginRight: theme.spacing(2), - }, - title: { - flexGrow: 1, - }, -})); - -export default function NavBar() { - const classes = useStyles(); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - - const handleMenu = event => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - - - - Shopping App - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    - ); -} diff --git a/exercises/exercise-7/src/constants/proptypes.constants.js b/exercises/exercise-7/src/constants/proptypes.constants.js deleted file mode 100644 index bfe9d9a..0000000 --- a/exercises/exercise-7/src/constants/proptypes.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ - PropTypes.array.isRequired, - PropTypes.object, - PropTypes.element, -]).isRequired; diff --git a/exercises/exercise-7/src/index.css b/exercises/exercise-7/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-7/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-7/src/index.js b/exercises/exercise-7/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-7/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-7/src/logo.svg b/exercises/exercise-7/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-7/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-7/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap b/exercises/exercise-7/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap deleted file mode 100644 index 5a12ea9..0000000 --- a/exercises/exercise-7/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`articles.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-7/src/modules/articles/__tests__/articles.actions.spec.js b/exercises/exercise-7/src/modules/articles/__tests__/articles.actions.spec.js deleted file mode 100644 index 3c97a72..0000000 --- a/exercises/exercise-7/src/modules/articles/__tests__/articles.actions.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import { RECEIVED_ARTICLES, requestArticles } from '../articles.actions'; - -jest.mock('../../../utils/api.utils', () => ({ - 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/exercises/exercise-7/src/modules/articles/__tests__/articles.context.spec.js b/exercises/exercise-7/src/modules/articles/__tests__/articles.context.spec.js deleted file mode 100644 index fd72859..0000000 --- a/exercises/exercise-7/src/modules/articles/__tests__/articles.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useArticles, - useArticlesState, - useArticlesDispatch, - ArticlesProvider, -} from '../articles.context'; - -describe('articles.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set to the articles state articles', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toMatchObject({ articles: [] }); - }); - }); - - 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/exercises/exercise-7/src/modules/articles/__tests__/articles.reducer.spec.js b/exercises/exercise-7/src/modules/articles/__tests__/articles.reducer.spec.js deleted file mode 100644 index 4a9dfc1..0000000 --- a/exercises/exercise-7/src/modules/articles/__tests__/articles.reducer.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/articles.actions.js b/exercises/exercise-7/src/modules/articles/articles.actions.js deleted file mode 100644 index f4cc0e3..0000000 --- a/exercises/exercise-7/src/modules/articles/articles.actions.js +++ /dev/null @@ -1,9 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/articles.context.js b/exercises/exercise-7/src/modules/articles/articles.context.js deleted file mode 100644 index 3f16199..0000000 --- a/exercises/exercise-7/src/modules/articles/articles.context.js +++ /dev/null @@ -1,52 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/articles.reducer.js b/exercises/exercise-7/src/modules/articles/articles.reducer.js deleted file mode 100644 index b6518e5..0000000 --- a/exercises/exercise-7/src/modules/articles/articles.reducer.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/articles.selectors.js b/exercises/exercise-7/src/modules/articles/articles.selectors.js deleted file mode 100644 index a5ec396..0000000 --- a/exercises/exercise-7/src/modules/articles/articles.selectors.js +++ /dev/null @@ -1,10 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap b/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap deleted file mode 100644 index c3be7f5..0000000 --- a/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`
    Snapshot should render correctly 1`] = ` - -`; diff --git a/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap b/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap deleted file mode 100644 index 755f8de..0000000 --- a/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - bar - -
    - - 1 - - - 1 - $ - -
    -
    - - - Add to Cart - - - See more - - -
    -
    -`; diff --git a/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap b/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap deleted file mode 100644 index d58ca86..0000000 --- a/exercises/exercise-7/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - - - - -`; diff --git a/exercises/exercise-7/src/modules/articles/components/__tests__/article.component.spec.js b/exercises/exercise-7/src/modules/articles/components/__tests__/article.component.spec.js deleted file mode 100644 index 3c381e8..0000000 --- a/exercises/exercise-7/src/modules/articles/components/__tests__/article.component.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Article } from '../article.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = id => shallow(
    ); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('
    ', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(getWrapper(MockedArticles[0].slug)).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect( - getWrapper(MockedArticles[0].slug).find('ArticleCard').prop('article'), - ).toMatchObject(MockedArticles[0]); - }); - - it('should be null when no id is passed', () => { - expect(getWrapper('foo')).toBeEmptyRender(); - }); - }); -}); diff --git a/exercises/exercise-7/src/modules/articles/components/__tests__/articleCard.component.spec.js b/exercises/exercise-7/src/modules/articles/components/__tests__/articleCard.component.spec.js deleted file mode 100644 index 1cf0a94..0000000 --- a/exercises/exercise-7/src/modules/articles/components/__tests__/articleCard.component.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticleCard } from '../articleCard.component'; - -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 { useCart } from '../../../cart/cart.context'; - -jest.mock('../../../cart/cart.context.js', () => ({ - useCart: jest.fn().mockReturnValue([null, jest.fn()]), -})); -let wrapper; - -const article = { - id: 'abc', - slug: 'foo', - name: 'bar', - year: '1', - image: 'baz', - price: 1, -}; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(CardMedia).prop('image')).toBe(article.image); - expect(wrapper.find(CardContent).find(Typography).first().text()).toBe( - article.name, - ); - expect( - wrapper.find(CardContent).find('div').find(Typography).first().text(), - ).toBe(article.year); - expect( - wrapper.find(CardContent).find('div').find(Typography).last().text(), - ).toBe(`${article.price} $`); - expect(wrapper.find(CardActions).find(Button).first().text()).toBe( - 'Add to Cart', - ); - expect(wrapper.find(CardActions).find(Button).last().text()).toBe( - 'See more', - ); - }); - }); - - describe('life cycles', () => { - it('should call useCart', () => { - expect(useCart).toHaveBeenCalled(); - }); - }); -}); diff --git a/exercises/exercise-7/src/modules/articles/components/__tests__/articlesList.component.spec.js b/exercises/exercise-7/src/modules/articles/components/__tests__/articlesList.component.spec.js deleted file mode 100644 index 1b62497..0000000 --- a/exercises/exercise-7/src/modules/articles/components/__tests__/articlesList.component.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Grid } from '@material-ui/core'; - -import { ArticlesList } from '../articlesList.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -let wrapper; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(Grid) - .children() - .map(node => node.prop('article')), - ).toMatchObject(MockedArticles); - }); - }); -}); diff --git a/exercises/exercise-7/src/modules/articles/components/article.component.js b/exercises/exercise-7/src/modules/articles/components/article.component.js deleted file mode 100644 index 4ac07c4..0000000 --- a/exercises/exercise-7/src/modules/articles/components/article.component.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/components/articleCard.component.js b/exercises/exercise-7/src/modules/articles/components/articleCard.component.js deleted file mode 100644 index 3f47f09..0000000 --- a/exercises/exercise-7/src/modules/articles/components/articleCard.component.js +++ /dev/null @@ -1,88 +0,0 @@ -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/exercises/exercise-7/src/modules/articles/components/articlesList.component.js b/exercises/exercise-7/src/modules/articles/components/articlesList.component.js deleted file mode 100644 index 099d842..0000000 --- a/exercises/exercise-7/src/modules/articles/components/articlesList.component.js +++ /dev/null @@ -1,19 +0,0 @@ -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/exercises/exercise-7/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap b/exercises/exercise-7/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap deleted file mode 100644 index 90bc395..0000000 --- a/exercises/exercise-7/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`cart.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-7/src/modules/cart/__tests__/cart.actions.spec.js b/exercises/exercise-7/src/modules/cart/__tests__/cart.actions.spec.js deleted file mode 100644 index 56b470f..0000000 --- a/exercises/exercise-7/src/modules/cart/__tests__/cart.actions.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -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/exercises/exercise-7/src/modules/cart/__tests__/cart.context.spec.js b/exercises/exercise-7/src/modules/cart/__tests__/cart.context.spec.js deleted file mode 100644 index b82c8ba..0000000 --- a/exercises/exercise-7/src/modules/cart/__tests__/cart.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useCart, - useCartState, - useCartDispatch, - CartProvider, -} from '../cart.context'; - -describe('cart.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set to the cart state cart', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toMatchObject({ articles: {} }); - }); - }); - - 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/exercises/exercise-7/src/modules/cart/__tests__/cart.reducer.spec.js b/exercises/exercise-7/src/modules/cart/__tests__/cart.reducer.spec.js deleted file mode 100644 index 95726f4..0000000 --- a/exercises/exercise-7/src/modules/cart/__tests__/cart.reducer.spec.js +++ /dev/null @@ -1,107 +0,0 @@ -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/exercises/exercise-7/src/modules/cart/cart.actions.js b/exercises/exercise-7/src/modules/cart/cart.actions.js deleted file mode 100644 index e7239d9..0000000 --- a/exercises/exercise-7/src/modules/cart/cart.actions.js +++ /dev/null @@ -1,6 +0,0 @@ -export const ADD_TO_CART = 'cart/ADD_TO_CART'; -export const REMOVE_FROM_CART = 'cart/REMOVE_FROM_CART'; - -export const addToCart = article => ({ type: ADD_TO_CART, article }); - -export const removeFromCart = id => ({ type: REMOVE_FROM_CART, id }); diff --git a/exercises/exercise-7/src/modules/cart/cart.context.js b/exercises/exercise-7/src/modules/cart/cart.context.js deleted file mode 100644 index 31aa978..0000000 --- a/exercises/exercise-7/src/modules/cart/cart.context.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; - -import { cartReducer, initialState } from './cart.reducer'; - -import { dispatchThunk } from '../../utils/context.utils'; -import { CHILDREN_PROP_TYPES } from '../../constants/proptypes.constants'; - -const CartStateContext = React.createContext(); -const CartDispatchContext = React.createContext(); - -const CartProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(cartReducer, initialState); - const getState = React.useCallback(() => state, [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/exercises/exercise-7/src/modules/cart/cart.reducer.js b/exercises/exercise-7/src/modules/cart/cart.reducer.js deleted file mode 100644 index dabcb26..0000000 --- a/exercises/exercise-7/src/modules/cart/cart.reducer.js +++ /dev/null @@ -1,75 +0,0 @@ -import { ADD_TO_CART, REMOVE_FROM_CART } from './cart.actions'; - -export const initialState = { - articles: {}, - total: 0, -}; - -export const cartReducer = (state, action) => { - switch (action.type) { - 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/exercises/exercise-7/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap b/exercises/exercise-7/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap deleted file mode 100644 index 8f8adc7..0000000 --- a/exercises/exercise-7/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap +++ /dev/null @@ -1,287 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly with articles 1`] = ` - - - - Cart - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Total Price: - $ - - - - - Check out - - - -`; diff --git a/exercises/exercise-7/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap b/exercises/exercise-7/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap deleted file mode 100644 index 23eb4a7..0000000 --- a/exercises/exercise-7/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly with articles 1`] = ` - - -

    - foo -

    -
    - - - -
    -`; diff --git a/exercises/exercise-7/src/modules/cart/components/__tests__/cart.component.spec.js b/exercises/exercise-7/src/modules/cart/components/__tests__/cart.component.spec.js deleted file mode 100644 index 0db4032..0000000 --- a/exercises/exercise-7/src/modules/cart/components/__tests__/cart.component.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Cart } from '../cart.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import { useCart } from '../../cart.context'; -import { Card, CardContent, List, Typography } from '@material-ui/core'; - -jest.mock('../../cart.context', () => ({ - useCart: jest.fn().mockReturnValue([{ articles: {} }, jest.fn()]), -})); - -const getWrapper = () => shallow(); - -const articles = MockedArticles.reduce( - (acc, curr) => ({ ...acc, [curr.id]: curr }), - {}, -); - -describe('', () => { - let wrapper; - beforeEach(() => { - jest.clearAllMocks(); - useCart.mockReturnValue([{ articles }, jest.fn()]); - wrapper = getWrapper(); - }); - - describe('Snapshot', () => { - it('should render correctly with articles', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(Typography).exists()).toBeTruthy(); - expect(wrapper.find(CardContent).exists()).toBeTruthy(); - expect(wrapper.find(List).exists()).toBeTruthy(); - }); - - it('should contain the correct list markup', () => { - expect(wrapper.find(List).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(List) - .children() - .map(node => node.prop('to')), - ).toEqual( - MockedArticles.reduce( - (acc, curr) => [...acc, `/articles/${curr.slug}`], - [], - ), - ); - }); - }); -}); diff --git a/exercises/exercise-7/src/modules/cart/components/__tests__/cartLayout.component.spec.js b/exercises/exercise-7/src/modules/cart/components/__tests__/cartLayout.component.spec.js deleted file mode 100644 index c069907..0000000 --- a/exercises/exercise-7/src/modules/cart/components/__tests__/cartLayout.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { CartLayout } from '../cartLayout.component'; -import { Grid } from '@material-ui/core'; - -jest.mock('../../cart.context', () => ({ - useCart: jest.fn().mockReturnValue([{ articles: {} }, jest.fn()]), -})); - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -describe('', () => { - let wrapper; - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - - describe('Snapshot', () => { - it('should render correctly with articles', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Cart').exists()).toBeTruthy(); - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find('p').text()).toBe('foo'); - }); - }); -}); diff --git a/exercises/exercise-7/src/modules/cart/components/cart.component.js b/exercises/exercise-7/src/modules/cart/components/cart.component.js deleted file mode 100644 index d0575d1..0000000 --- a/exercises/exercise-7/src/modules/cart/components/cart.component.js +++ /dev/null @@ -1,97 +0,0 @@ -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'; - -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 => ( - - - - - - - - - - ))} - - - Total Price: {total} $ - - - - - - - ); -} diff --git a/exercises/exercise-7/src/modules/cart/components/cartLayout.component.js b/exercises/exercise-7/src/modules/cart/components/cartLayout.component.js deleted file mode 100644 index af37943..0000000 --- a/exercises/exercise-7/src/modules/cart/components/cartLayout.component.js +++ /dev/null @@ -1,22 +0,0 @@ -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/exercises/exercise-7/src/pages/__tests__/__snapshots__/about.page.spec.js.snap b/exercises/exercise-7/src/pages/__tests__/__snapshots__/about.page.spec.js.snap deleted file mode 100644 index 71bc0e1..0000000 --- a/exercises/exercise-7/src/pages/__tests__/__snapshots__/about.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - About -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-7/src/pages/__tests__/__snapshots__/article.page.spec.js.snap b/exercises/exercise-7/src/pages/__tests__/__snapshots__/article.page.spec.js.snap deleted file mode 100644 index e75a78e..0000000 --- a/exercises/exercise-7/src/pages/__tests__/__snapshots__/article.page.spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - -

    - Article - foo -

    - - Return to Home - -
    - -
    - - -`; diff --git a/exercises/exercise-7/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap b/exercises/exercise-7/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap deleted file mode 100644 index 8c0408c..0000000 --- a/exercises/exercise-7/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - Contact -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-7/src/pages/__tests__/__snapshots__/home.page.spec.js.snap b/exercises/exercise-7/src/pages/__tests__/__snapshots__/home.page.spec.js.snap deleted file mode 100644 index ef599e5..0000000 --- a/exercises/exercise-7/src/pages/__tests__/__snapshots__/home.page.spec.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -

    - Home Page -

    - - - -
    -`; diff --git a/exercises/exercise-7/src/pages/__tests__/about.page.spec.js b/exercises/exercise-7/src/pages/__tests__/about.page.spec.js deleted file mode 100644 index 6d7eb91..0000000 --- a/exercises/exercise-7/src/pages/__tests__/about.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AboutPage } from '../about.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-7/src/pages/__tests__/article.page.spec.js b/exercises/exercise-7/src/pages/__tests__/article.page.spec.js deleted file mode 100644 index a32b00b..0000000 --- a/exercises/exercise-7/src/pages/__tests__/article.page.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticlePage } from '../article.page'; - -let wrapper; - -jest.mock('react-router-dom', () => ({ - useParams: jest.fn().mockReturnValue({ id: 'foo' }), -})); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`Article`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-7/src/pages/__tests__/contact.page.spec.js b/exercises/exercise-7/src/pages/__tests__/contact.page.spec.js deleted file mode 100644 index 0e29df1..0000000 --- a/exercises/exercise-7/src/pages/__tests__/contact.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ContactPage } from '../contact.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-7/src/pages/__tests__/home.page.spec.js b/exercises/exercise-7/src/pages/__tests__/home.page.spec.js deleted file mode 100644 index 11eb6d9..0000000 --- a/exercises/exercise-7/src/pages/__tests__/home.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { HomePage } from '../home.page'; - -let wrapper; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect(wrapper.find(`ArticlesList`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-7/src/pages/about.page.js b/exercises/exercise-7/src/pages/about.page.js deleted file mode 100644 index 567e4b9..0000000 --- a/exercises/exercise-7/src/pages/about.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-7/src/pages/article.page.js b/exercises/exercise-7/src/pages/article.page.js deleted file mode 100644 index 09193c7..0000000 --- a/exercises/exercise-7/src/pages/article.page.js +++ /dev/null @@ -1,27 +0,0 @@ -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/exercises/exercise-7/src/pages/contact.page.js b/exercises/exercise-7/src/pages/contact.page.js deleted file mode 100644 index 1e2e74b..0000000 --- a/exercises/exercise-7/src/pages/contact.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-7/src/pages/home.page.js b/exercises/exercise-7/src/pages/home.page.js deleted file mode 100644 index 9ecdf58..0000000 --- a/exercises/exercise-7/src/pages/home.page.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-8/README.md b/exercises/exercise-8/README.md deleted file mode 100644 index d89e5b0..0000000 --- a/exercises/exercise-8/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# 8/ Custom routing, good practices. Adding checkout - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | -------------- | -| Create | src/modules/checkout/checkout.component.js | {Checkout} | -| Create | src/modules/checkout/components/review.component.js | {Review} | -| Create | src/modules/checkout/components/addressForm.component.js | {AddressForm} | -| Create | src/modules/checkout/components/paymentForm.component.js | {PaymentForm} | -| Modify | src/App.js | {App} | -| Modify | src/pages/checkout.page.js | {CheckoutPage} | - -## TL;DR - -Let's create the checkout module ! - -We need a Checkout page that needs a logged in user. So we also need a Login page. -We'll mock the user api and authentication process for now. -We need to bo redirected to the login page on some routes, not all. - -## Step by step - -### src/modules/checkout/checkout.component.js diff --git a/exercises/exercise-8/src/App.js b/exercises/exercise-8/src/App.js deleted file mode 100644 index 5abb118..0000000 --- a/exercises/exercise-8/src/App.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-8/src/__hints__/dialog.js b/exercises/exercise-8/src/__hints__/dialog.js deleted file mode 100644 index 079a0bc..0000000 --- a/exercises/exercise-8/src/__hints__/dialog.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import instructionsMd from './instructions.md'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; -import emoji from 'emoji-dictionary'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - ['& blockquote']: { - boxShadow: - '0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%)', - borderRadius: '4px', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - padding: '20px', - borderLeft: '5px solid #3f51b5', - }, - - ['& p']: { - lineHeight: '25px', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-8/src/__hints__/instructions.md b/exercises/exercise-8/src/__hints__/instructions.md deleted file mode 100644 index 12802c5..0000000 --- a/exercises/exercise-8/src/__hints__/instructions.md +++ /dev/null @@ -1,24 +0,0 @@ -# 8/ Custom routing, good practices. Adding checkout - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | -------------- | -| Create | src/modules/checkout/checkout.component.js | {Checkout} | -| Create | src/modules/checkout/components/review.component.js | {Review} | -| Create | src/modules/checkout/components/addressForm.component.js | {AddressForm} | -| Create | src/modules/checkout/components/paymentForm.component.js | {PaymentForm} | -| Modify | src/App.js | {App} | -| Modify | src/pages/checkout.page.js | {CheckoutPage} | - -## TL;DR - -Let's create the checkout modules ! - -We need a Checkout page that needs a logged in user. So we also need a Login page. -We'll mock the user api and authentication process for now. -We need to bo redirected to the login page on some routes, not all. - -## Step by step - -To begin with, let's duplicate the **articles** modules and rename it user, it should be pretty step forward to adapt it. - -### src/modules/checkout/checkout.component.js diff --git a/exercises/exercise-8/src/__tests__/App.spec.js b/exercises/exercise-8/src/__tests__/App.spec.js deleted file mode 100644 index 34e1bc5..0000000 --- a/exercises/exercise-8/src/__tests__/App.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -describe('App', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); diff --git a/exercises/exercise-8/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-8/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index f130870..0000000 --- a/exercises/exercise-8/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App should render correctly 1`] = ` - - - - - -`; diff --git a/exercises/exercise-8/src/components/__tests__/__snapshots__/layout.component.spec.js.snap b/exercises/exercise-8/src/components/__tests__/__snapshots__/layout.component.spec.js.snap deleted file mode 100644 index 3de464d..0000000 --- a/exercises/exercise-8/src/components/__tests__/__snapshots__/layout.component.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - -

    - foo -

    -
    -
    -`; diff --git a/exercises/exercise-8/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap b/exercises/exercise-8/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap deleted file mode 100644 index 7495a9b..0000000 --- a/exercises/exercise-8/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - Shopping App - - - - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    -`; diff --git a/exercises/exercise-8/src/components/__tests__/layout.component.spec.js b/exercises/exercise-8/src/components/__tests__/layout.component.spec.js deleted file mode 100644 index 6add5d5..0000000 --- a/exercises/exercise-8/src/components/__tests__/layout.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Layout } from '../layout.component'; - -let wrapper; - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('NavBar').exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-8/src/components/__tests__/navbar.component.spec.js b/exercises/exercise-8/src/components/__tests__/navbar.component.spec.js deleted file mode 100644 index f03d956..0000000 --- a/exercises/exercise-8/src/components/__tests__/navbar.component.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AppBar, IconButton, Menu, Typography } from '@material-ui/core'; - -import NavBar from '../navbar.component'; - -let wrapper; -jest.mock('../../modules/user/user.context.js', () => ({ - useUserState: jest.fn().mockReturnValue({ user: null }), - useUser: jest.fn().mockReturnValue([{ user: null }, jest.fn()]), -})); -const getWrapper = () => shallow(); - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(AppBar).exists()).toBeTruthy(); - expect(wrapper.find(Typography).first().text()).toBe('Shopping App'); - }); - }); - - describe('Interaction checks', () => { - it('should sur anchorEl from clicked item', () => { - expect(wrapper.find(Menu).prop('open')).toBeFalsy(); - wrapper.find(IconButton).last().prop('onClick')({ currentTarget: 'foo' }); - wrapper.update(); - expect(wrapper.find(Menu).prop('open')).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-8/src/components/layout.component.js b/exercises/exercise-8/src/components/layout.component.js deleted file mode 100644 index d8bede0..0000000 --- a/exercises/exercise-8/src/components/layout.component.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-8/src/components/navbar.component.js b/exercises/exercise-8/src/components/navbar.component.js deleted file mode 100644 index b6eb8d8..0000000 --- a/exercises/exercise-8/src/components/navbar.component.js +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { Link } 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/PowerSettingsNewOutlined'; - -import { isUserConnected } from '../modules/user/user.selectors'; -import { useUser } from '../modules/user/user.context'; -import { login, 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 handleMenu = React.useCallback(event => { - setAnchorEl(event.currentTarget); - }, []); - - const handleClose = React.useCallback(() => { - setAnchorEl(null); - }, []); - - const logInAndOut = React.useCallback(() => { - dispatch(isConnected ? logout() : login()); - }, [dispatch, isConnected]); - - return ( - - - - Shopping App - - - - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    - ); -} diff --git a/exercises/exercise-8/src/constants/proptypes.constants.js b/exercises/exercise-8/src/constants/proptypes.constants.js deleted file mode 100644 index bfe9d9a..0000000 --- a/exercises/exercise-8/src/constants/proptypes.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ - PropTypes.array.isRequired, - PropTypes.object, - PropTypes.element, -]).isRequired; diff --git a/exercises/exercise-8/src/hooks/useInput.hook.js b/exercises/exercise-8/src/hooks/useInput.hook.js deleted file mode 100644 index 3b53d0b..0000000 --- a/exercises/exercise-8/src/hooks/useInput.hook.js +++ /dev/null @@ -1,9 +0,0 @@ -import { useState } from 'react'; - -export const useInput = () => { - const [inputValue, setInputValue] = useState(''); - - const handleChange = e => setInputValue(e.target.value); - - return [inputValue, handleChange]; -}; diff --git a/exercises/exercise-8/src/index.css b/exercises/exercise-8/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-8/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-8/src/index.js b/exercises/exercise-8/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-8/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-8/src/logo.svg b/exercises/exercise-8/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-8/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-8/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap b/exercises/exercise-8/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap deleted file mode 100644 index 5a12ea9..0000000 --- a/exercises/exercise-8/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`articles.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-8/src/modules/articles/__tests__/articles.actions.spec.js b/exercises/exercise-8/src/modules/articles/__tests__/articles.actions.spec.js deleted file mode 100644 index 3c97a72..0000000 --- a/exercises/exercise-8/src/modules/articles/__tests__/articles.actions.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import { RECEIVED_ARTICLES, requestArticles } from '../articles.actions'; - -jest.mock('../../../utils/api.utils', () => ({ - 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/exercises/exercise-8/src/modules/articles/__tests__/articles.context.spec.js b/exercises/exercise-8/src/modules/articles/__tests__/articles.context.spec.js deleted file mode 100644 index fd72859..0000000 --- a/exercises/exercise-8/src/modules/articles/__tests__/articles.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useArticles, - useArticlesState, - useArticlesDispatch, - ArticlesProvider, -} from '../articles.context'; - -describe('articles.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set to the articles state articles', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toMatchObject({ articles: [] }); - }); - }); - - 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/exercises/exercise-8/src/modules/articles/__tests__/articles.reducer.spec.js b/exercises/exercise-8/src/modules/articles/__tests__/articles.reducer.spec.js deleted file mode 100644 index 4a9dfc1..0000000 --- a/exercises/exercise-8/src/modules/articles/__tests__/articles.reducer.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/articles.actions.js b/exercises/exercise-8/src/modules/articles/articles.actions.js deleted file mode 100644 index f4cc0e3..0000000 --- a/exercises/exercise-8/src/modules/articles/articles.actions.js +++ /dev/null @@ -1,9 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/articles.context.js b/exercises/exercise-8/src/modules/articles/articles.context.js deleted file mode 100644 index 3f16199..0000000 --- a/exercises/exercise-8/src/modules/articles/articles.context.js +++ /dev/null @@ -1,52 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/articles.reducer.js b/exercises/exercise-8/src/modules/articles/articles.reducer.js deleted file mode 100644 index b6518e5..0000000 --- a/exercises/exercise-8/src/modules/articles/articles.reducer.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/articles.selectors.js b/exercises/exercise-8/src/modules/articles/articles.selectors.js deleted file mode 100644 index a5ec396..0000000 --- a/exercises/exercise-8/src/modules/articles/articles.selectors.js +++ /dev/null @@ -1,10 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap b/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap deleted file mode 100644 index c3be7f5..0000000 --- a/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`
    Snapshot should render correctly 1`] = ` - -`; diff --git a/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap b/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap deleted file mode 100644 index 755f8de..0000000 --- a/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - bar - -
    - - 1 - - - 1 - $ - -
    -
    - - - Add to Cart - - - See more - - -
    -
    -`; diff --git a/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap b/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap deleted file mode 100644 index 09117d2..0000000 --- a/exercises/exercise-8/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - - - - -`; diff --git a/exercises/exercise-8/src/modules/articles/components/__tests__/article.component.spec.js b/exercises/exercise-8/src/modules/articles/components/__tests__/article.component.spec.js deleted file mode 100644 index 3c381e8..0000000 --- a/exercises/exercise-8/src/modules/articles/components/__tests__/article.component.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Article } from '../article.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = id => shallow(
    ); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('
    ', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(getWrapper(MockedArticles[0].slug)).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect( - getWrapper(MockedArticles[0].slug).find('ArticleCard').prop('article'), - ).toMatchObject(MockedArticles[0]); - }); - - it('should be null when no id is passed', () => { - expect(getWrapper('foo')).toBeEmptyRender(); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/articles/components/__tests__/articleCard.component.spec.js b/exercises/exercise-8/src/modules/articles/components/__tests__/articleCard.component.spec.js deleted file mode 100644 index 1cf0a94..0000000 --- a/exercises/exercise-8/src/modules/articles/components/__tests__/articleCard.component.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticleCard } from '../articleCard.component'; - -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 { useCart } from '../../../cart/cart.context'; - -jest.mock('../../../cart/cart.context.js', () => ({ - useCart: jest.fn().mockReturnValue([null, jest.fn()]), -})); -let wrapper; - -const article = { - id: 'abc', - slug: 'foo', - name: 'bar', - year: '1', - image: 'baz', - price: 1, -}; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(CardMedia).prop('image')).toBe(article.image); - expect(wrapper.find(CardContent).find(Typography).first().text()).toBe( - article.name, - ); - expect( - wrapper.find(CardContent).find('div').find(Typography).first().text(), - ).toBe(article.year); - expect( - wrapper.find(CardContent).find('div').find(Typography).last().text(), - ).toBe(`${article.price} $`); - expect(wrapper.find(CardActions).find(Button).first().text()).toBe( - 'Add to Cart', - ); - expect(wrapper.find(CardActions).find(Button).last().text()).toBe( - 'See more', - ); - }); - }); - - describe('life cycles', () => { - it('should call useCart', () => { - expect(useCart).toHaveBeenCalled(); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/articles/components/__tests__/articlesList.component.spec.js b/exercises/exercise-8/src/modules/articles/components/__tests__/articlesList.component.spec.js deleted file mode 100644 index 1b62497..0000000 --- a/exercises/exercise-8/src/modules/articles/components/__tests__/articlesList.component.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Grid } from '@material-ui/core'; - -import { ArticlesList } from '../articlesList.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -let wrapper; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(Grid) - .children() - .map(node => node.prop('article')), - ).toMatchObject(MockedArticles); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/articles/components/article.component.js b/exercises/exercise-8/src/modules/articles/components/article.component.js deleted file mode 100644 index 4ac07c4..0000000 --- a/exercises/exercise-8/src/modules/articles/components/article.component.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/components/articleCard.component.js b/exercises/exercise-8/src/modules/articles/components/articleCard.component.js deleted file mode 100644 index 3f47f09..0000000 --- a/exercises/exercise-8/src/modules/articles/components/articleCard.component.js +++ /dev/null @@ -1,88 +0,0 @@ -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/exercises/exercise-8/src/modules/articles/components/articlesList.component.js b/exercises/exercise-8/src/modules/articles/components/articlesList.component.js deleted file mode 100644 index fa8e8a5..0000000 --- a/exercises/exercise-8/src/modules/articles/components/articlesList.component.js +++ /dev/null @@ -1,19 +0,0 @@ -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/exercises/exercise-8/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap b/exercises/exercise-8/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap deleted file mode 100644 index 90bc395..0000000 --- a/exercises/exercise-8/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`cart.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-8/src/modules/cart/__tests__/cart.actions.spec.js b/exercises/exercise-8/src/modules/cart/__tests__/cart.actions.spec.js deleted file mode 100644 index 56b470f..0000000 --- a/exercises/exercise-8/src/modules/cart/__tests__/cart.actions.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -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/exercises/exercise-8/src/modules/cart/__tests__/cart.context.spec.js b/exercises/exercise-8/src/modules/cart/__tests__/cart.context.spec.js deleted file mode 100644 index 1bac696..0000000 --- a/exercises/exercise-8/src/modules/cart/__tests__/cart.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useCart, - useCartState, - useCartDispatch, - CartProvider, -} from '../cart.context'; - -describe('cart.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set to the cart state cart', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toEqual({ articles: {}, total: 0 }); - }); - }); - - 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/exercises/exercise-8/src/modules/cart/__tests__/cart.reducer.spec.js b/exercises/exercise-8/src/modules/cart/__tests__/cart.reducer.spec.js deleted file mode 100644 index 95726f4..0000000 --- a/exercises/exercise-8/src/modules/cart/__tests__/cart.reducer.spec.js +++ /dev/null @@ -1,107 +0,0 @@ -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/exercises/exercise-8/src/modules/cart/cart.actions.js b/exercises/exercise-8/src/modules/cart/cart.actions.js deleted file mode 100644 index e7239d9..0000000 --- a/exercises/exercise-8/src/modules/cart/cart.actions.js +++ /dev/null @@ -1,6 +0,0 @@ -export const ADD_TO_CART = 'cart/ADD_TO_CART'; -export const REMOVE_FROM_CART = 'cart/REMOVE_FROM_CART'; - -export const addToCart = article => ({ type: ADD_TO_CART, article }); - -export const removeFromCart = id => ({ type: REMOVE_FROM_CART, id }); diff --git a/exercises/exercise-8/src/modules/cart/cart.context.js b/exercises/exercise-8/src/modules/cart/cart.context.js deleted file mode 100644 index 5aa097c..0000000 --- a/exercises/exercise-8/src/modules/cart/cart.context.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; - -import { cartReducer, initialState } from './cart.reducer'; - -import { dispatchThunk } from '../../utils/context.utils'; -import { CHILDREN_PROP_TYPES } from '../../constants/proptypes.constants'; - -const CartStateContext = React.createContext(); -const CartDispatchContext = React.createContext(); - -const CartProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(cartReducer, initialState); - const getState = React.useCallback(() => state, [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/exercises/exercise-8/src/modules/cart/cart.reducer.js b/exercises/exercise-8/src/modules/cart/cart.reducer.js deleted file mode 100644 index dabcb26..0000000 --- a/exercises/exercise-8/src/modules/cart/cart.reducer.js +++ /dev/null @@ -1,75 +0,0 @@ -import { ADD_TO_CART, REMOVE_FROM_CART } from './cart.actions'; - -export const initialState = { - articles: {}, - total: 0, -}; - -export const cartReducer = (state, action) => { - switch (action.type) { - 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/exercises/exercise-8/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap b/exercises/exercise-8/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap deleted file mode 100644 index b622b4f..0000000 --- a/exercises/exercise-8/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap +++ /dev/null @@ -1,300 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly with articles 1`] = ` - - - - Cart - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Total Price: - $ - - - - - Check out - - - -`; diff --git a/exercises/exercise-8/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap b/exercises/exercise-8/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap deleted file mode 100644 index 23eb4a7..0000000 --- a/exercises/exercise-8/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly with articles 1`] = ` - - -

    - foo -

    -
    - - - -
    -`; diff --git a/exercises/exercise-8/src/modules/cart/components/__tests__/cart.component.spec.js b/exercises/exercise-8/src/modules/cart/components/__tests__/cart.component.spec.js deleted file mode 100644 index 0db4032..0000000 --- a/exercises/exercise-8/src/modules/cart/components/__tests__/cart.component.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Cart } from '../cart.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import { useCart } from '../../cart.context'; -import { Card, CardContent, List, Typography } from '@material-ui/core'; - -jest.mock('../../cart.context', () => ({ - useCart: jest.fn().mockReturnValue([{ articles: {} }, jest.fn()]), -})); - -const getWrapper = () => shallow(); - -const articles = MockedArticles.reduce( - (acc, curr) => ({ ...acc, [curr.id]: curr }), - {}, -); - -describe('', () => { - let wrapper; - beforeEach(() => { - jest.clearAllMocks(); - useCart.mockReturnValue([{ articles }, jest.fn()]); - wrapper = getWrapper(); - }); - - describe('Snapshot', () => { - it('should render correctly with articles', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(Typography).exists()).toBeTruthy(); - expect(wrapper.find(CardContent).exists()).toBeTruthy(); - expect(wrapper.find(List).exists()).toBeTruthy(); - }); - - it('should contain the correct list markup', () => { - expect(wrapper.find(List).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(List) - .children() - .map(node => node.prop('to')), - ).toEqual( - MockedArticles.reduce( - (acc, curr) => [...acc, `/articles/${curr.slug}`], - [], - ), - ); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/cart/components/__tests__/cartLayout.component.spec.js b/exercises/exercise-8/src/modules/cart/components/__tests__/cartLayout.component.spec.js deleted file mode 100644 index c069907..0000000 --- a/exercises/exercise-8/src/modules/cart/components/__tests__/cartLayout.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { CartLayout } from '../cartLayout.component'; -import { Grid } from '@material-ui/core'; - -jest.mock('../../cart.context', () => ({ - useCart: jest.fn().mockReturnValue([{ articles: {} }, jest.fn()]), -})); - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -describe('', () => { - let wrapper; - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - - describe('Snapshot', () => { - it('should render correctly with articles', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Cart').exists()).toBeTruthy(); - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find('p').text()).toBe('foo'); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/cart/components/cart.component.js b/exercises/exercise-8/src/modules/cart/components/cart.component.js deleted file mode 100644 index 8ad6d65..0000000 --- a/exercises/exercise-8/src/modules/cart/components/cart.component.js +++ /dev/null @@ -1,99 +0,0 @@ -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 => ( - - - - - - - - - - ))} - - - Total Price: {total} $ - - - - - - - ); -} diff --git a/exercises/exercise-8/src/modules/cart/components/cartLayout.component.js b/exercises/exercise-8/src/modules/cart/components/cartLayout.component.js deleted file mode 100644 index af37943..0000000 --- a/exercises/exercise-8/src/modules/cart/components/cartLayout.component.js +++ /dev/null @@ -1,22 +0,0 @@ -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/exercises/exercise-8/src/modules/routing/__tests__/rooting.hooks.spec.js b/exercises/exercise-8/src/modules/routing/__tests__/rooting.hooks.spec.js deleted file mode 100644 index 2c01a79..0000000 --- a/exercises/exercise-8/src/modules/routing/__tests__/rooting.hooks.spec.js +++ /dev/null @@ -1,151 +0,0 @@ -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 { shallow } from 'enzyme'; - -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); - }); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/routing/components/__tests__/__snapshots__/routes.component.spec.js.snap b/exercises/exercise-8/src/modules/routing/components/__tests__/__snapshots__/routes.component.spec.js.snap deleted file mode 100644 index 22561c6..0000000 --- a/exercises/exercise-8/src/modules/routing/components/__tests__/__snapshots__/routes.component.spec.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/exercises/exercise-8/src/modules/routing/components/__tests__/routes.component.spec.js b/exercises/exercise-8/src/modules/routing/components/__tests__/routes.component.spec.js deleted file mode 100644 index 2f75c75..0000000 --- a/exercises/exercise-8/src/modules/routing/components/__tests__/routes.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; -import { AppRoutes } from '../routes.component'; -import { ROUTES_PATHS_BY_NAMES } from '../../routing.constants'; -import { Route } from 'react-router-dom'; - -jest.mock('../../routing.hooks.js', () => ({ useLoginRedirect: jest.fn() })); - -let wrapper; - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - beforeEach(() => { - wrapper = shallow(); - }); - - it('should have routes for all paths', () => { - expect( - wrapper - .find(Route) - .map(node => node.prop('path')) - .sort(), - ).toMatchObject(Object.values(ROUTES_PATHS_BY_NAMES).sort()); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/routing/components/routes.component.js b/exercises/exercise-8/src/modules/routing/components/routes.component.js deleted file mode 100644 index 7aef6e0..0000000 --- a/exercises/exercise-8/src/modules/routing/components/routes.component.js +++ /dev/null @@ -1,50 +0,0 @@ -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/exercises/exercise-8/src/modules/routing/routing.constants.js b/exercises/exercise-8/src/modules/routing/routing.constants.js deleted file mode 100644 index 73687be..0000000 --- a/exercises/exercise-8/src/modules/routing/routing.constants.js +++ /dev/null @@ -1,13 +0,0 @@ -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.login, - ROUTES_PATHS_BY_NAMES.checkout, -]; diff --git a/exercises/exercise-8/src/modules/routing/routing.hooks.js b/exercises/exercise-8/src/modules/routing/routing.hooks.js deleted file mode 100644 index d47402b..0000000 --- a/exercises/exercise-8/src/modules/routing/routing.hooks.js +++ /dev/null @@ -1,35 +0,0 @@ -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/exercises/exercise-8/src/modules/user/__tests__/__snapshots__/user.context.spec.js.snap b/exercises/exercise-8/src/modules/user/__tests__/__snapshots__/user.context.spec.js.snap deleted file mode 100644 index 041c67a..0000000 --- a/exercises/exercise-8/src/modules/user/__tests__/__snapshots__/user.context.spec.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`user.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-8/src/modules/user/__tests__/user.actions.spec.js b/exercises/exercise-8/src/modules/user/__tests__/user.actions.spec.js deleted file mode 100644 index 0a4f7b1..0000000 --- a/exercises/exercise-8/src/modules/user/__tests__/user.actions.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -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/exercises/exercise-8/src/modules/user/__tests__/user.context.spec.js b/exercises/exercise-8/src/modules/user/__tests__/user.context.spec.js deleted file mode 100644 index cc5b5e5..0000000 --- a/exercises/exercise-8/src/modules/user/__tests__/user.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useUser, - useUserState, - useUserDispatch, - UserProvider, -} from '../user.context'; - -describe('user.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toMatchObject({ user: null }); - }); - }); - - 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/exercises/exercise-8/src/modules/user/__tests__/user.reducer.spec.js b/exercises/exercise-8/src/modules/user/__tests__/user.reducer.spec.js deleted file mode 100644 index 3a23a5d..0000000 --- a/exercises/exercise-8/src/modules/user/__tests__/user.reducer.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-8/src/modules/user/__tests__/user.selectors.spec.js b/exercises/exercise-8/src/modules/user/__tests__/user.selectors.spec.js deleted file mode 100644 index 5a96502..0000000 --- a/exercises/exercise-8/src/modules/user/__tests__/user.selectors.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -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/exercises/exercise-8/src/modules/user/components/__tests__/__snapshots__/login.component.spec.js.snap b/exercises/exercise-8/src/modules/user/components/__tests__/__snapshots__/login.component.spec.js.snap deleted file mode 100644 index b7709d8..0000000 --- a/exercises/exercise-8/src/modules/user/components/__tests__/__snapshots__/login.component.spec.js.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -
    - - - - - Sign in - -
    - - - - } - label="Remember me" - /> - - Sign In - - - - - Forgot password? - - - - - Don't have an account? Sign Up - - - - -
    -
    -`; diff --git a/exercises/exercise-8/src/modules/user/components/__tests__/login.component.spec.js b/exercises/exercise-8/src/modules/user/components/__tests__/login.component.spec.js deleted file mode 100644 index d5adb6c..0000000 --- a/exercises/exercise-8/src/modules/user/components/__tests__/login.component.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Login } from '../login.component'; -import { useInput } from '../../../../hooks/useInput.hook'; -import { Button, TextField } from '@material-ui/core'; -import { login } from '../../user.actions'; - -jest.mock('../../user.context.js', () => ({ - useUserDispatch: jest - .fn() - .mockReturnValue(args => (typeof args === 'function' ? args() : args)), -})); - -jest.mock('../../user.actions.js', () => ({ login: jest.fn() })); - -jest.mock('../../../../hooks/useInput.hook.js', () => ({ - useInput: jest.fn().mockReturnValue(['', jest.fn()]), -})); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = shallow(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should render a Button', () => { - expect(wrapper.find(Button).text()).toBe('Sign In'); - }); - - describe('TextFields', () => { - it('should render a first TextField', () => { - expect(wrapper.find(TextField).first().prop('name')).toBe('email'); - }); - it('should render a second TextField', () => { - expect(wrapper.find(TextField).last().prop('name')).toBe('password'); - }); - }); - }); - - describe('Hooks', () => { - it('should call useInput twice', () => { - expect(useInput).toHaveBeenCalled(); - }); - }); - - describe('Interaction checks', () => { - it('should dispatch login on submit', () => { - shallow() - .find('form') - .invoke('onSubmit')({ preventDefault: jest.fn() }); - - expect(login).toBeCalled(); - }); - }); -}); diff --git a/exercises/exercise-8/src/modules/user/components/login.component.js b/exercises/exercise-8/src/modules/user/components/login.component.js deleted file mode 100644 index 0f2a1a7..0000000 --- a/exercises/exercise-8/src/modules/user/components/login.component.js +++ /dev/null @@ -1,116 +0,0 @@ -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/exercises/exercise-8/src/modules/user/user.actions.js b/exercises/exercise-8/src/modules/user/user.actions.js deleted file mode 100644 index 27c4d2b..0000000 --- a/exercises/exercise-8/src/modules/user/user.actions.js +++ /dev/null @@ -1,32 +0,0 @@ -import { signIn, signOut } from '@react-course-v2/api'; -import { getUser } from './user.selectors'; - -export const LOGIN = 'user/LOGIN'; -export const LOGOUT = 'user/LOGOUT'; - -export const login = (email, password) => async dispatch => { - try { - const user = await signIn(email, password); - - 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/exercises/exercise-8/src/modules/user/user.context.js b/exercises/exercise-8/src/modules/user/user.context.js deleted file mode 100644 index 801772c..0000000 --- a/exercises/exercise-8/src/modules/user/user.context.js +++ /dev/null @@ -1,53 +0,0 @@ -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/exercises/exercise-8/src/modules/user/user.hooks.js b/exercises/exercise-8/src/modules/user/user.hooks.js deleted file mode 100644 index ba4cad3..0000000 --- a/exercises/exercise-8/src/modules/user/user.hooks.js +++ /dev/null @@ -1,5 +0,0 @@ -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/exercises/exercise-8/src/modules/user/user.reducer.js b/exercises/exercise-8/src/modules/user/user.reducer.js deleted file mode 100644 index bb50cc5..0000000 --- a/exercises/exercise-8/src/modules/user/user.reducer.js +++ /dev/null @@ -1,25 +0,0 @@ -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/exercises/exercise-8/src/modules/user/user.selectors.js b/exercises/exercise-8/src/modules/user/user.selectors.js deleted file mode 100644 index 4d798c1..0000000 --- a/exercises/exercise-8/src/modules/user/user.selectors.js +++ /dev/null @@ -1,2 +0,0 @@ -export const isUserConnected = ({ user }) => !!user; -export const getUser = ({ user }) => user; diff --git a/exercises/exercise-8/src/pages/__tests__/__snapshots__/about.page.spec.js.snap b/exercises/exercise-8/src/pages/__tests__/__snapshots__/about.page.spec.js.snap deleted file mode 100644 index 71bc0e1..0000000 --- a/exercises/exercise-8/src/pages/__tests__/__snapshots__/about.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - About -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-8/src/pages/__tests__/__snapshots__/article.page.spec.js.snap b/exercises/exercise-8/src/pages/__tests__/__snapshots__/article.page.spec.js.snap deleted file mode 100644 index e75a78e..0000000 --- a/exercises/exercise-8/src/pages/__tests__/__snapshots__/article.page.spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - -

    - Article - foo -

    - - Return to Home - -
    - -
    - - -`; diff --git a/exercises/exercise-8/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap b/exercises/exercise-8/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap deleted file mode 100644 index 8c0408c..0000000 --- a/exercises/exercise-8/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - Contact -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-8/src/pages/__tests__/__snapshots__/home.page.spec.js.snap b/exercises/exercise-8/src/pages/__tests__/__snapshots__/home.page.spec.js.snap deleted file mode 100644 index ef599e5..0000000 --- a/exercises/exercise-8/src/pages/__tests__/__snapshots__/home.page.spec.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -

    - Home Page -

    - - - -
    -`; diff --git a/exercises/exercise-8/src/pages/__tests__/about.page.spec.js b/exercises/exercise-8/src/pages/__tests__/about.page.spec.js deleted file mode 100644 index 6d7eb91..0000000 --- a/exercises/exercise-8/src/pages/__tests__/about.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AboutPage } from '../about.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-8/src/pages/__tests__/article.page.spec.js b/exercises/exercise-8/src/pages/__tests__/article.page.spec.js deleted file mode 100644 index a32b00b..0000000 --- a/exercises/exercise-8/src/pages/__tests__/article.page.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticlePage } from '../article.page'; - -let wrapper; - -jest.mock('react-router-dom', () => ({ - useParams: jest.fn().mockReturnValue({ id: 'foo' }), -})); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`Article`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-8/src/pages/__tests__/contact.page.spec.js b/exercises/exercise-8/src/pages/__tests__/contact.page.spec.js deleted file mode 100644 index 0e29df1..0000000 --- a/exercises/exercise-8/src/pages/__tests__/contact.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ContactPage } from '../contact.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-8/src/pages/__tests__/home.page.spec.js b/exercises/exercise-8/src/pages/__tests__/home.page.spec.js deleted file mode 100644 index 11eb6d9..0000000 --- a/exercises/exercise-8/src/pages/__tests__/home.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { HomePage } from '../home.page'; - -let wrapper; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect(wrapper.find(`ArticlesList`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-8/src/pages/about.page.js b/exercises/exercise-8/src/pages/about.page.js deleted file mode 100644 index 567e4b9..0000000 --- a/exercises/exercise-8/src/pages/about.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-8/src/pages/article.page.js b/exercises/exercise-8/src/pages/article.page.js deleted file mode 100644 index 09193c7..0000000 --- a/exercises/exercise-8/src/pages/article.page.js +++ /dev/null @@ -1,27 +0,0 @@ -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/exercises/exercise-8/src/pages/checkout.page.js b/exercises/exercise-8/src/pages/checkout.page.js deleted file mode 100644 index 47b4279..0000000 --- a/exercises/exercise-8/src/pages/checkout.page.js +++ /dev/null @@ -1,24 +0,0 @@ -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'; -import { CartLayout } from '../modules/cart/components/cartLayout.component'; - -export const CheckoutPage = () => { - return ( - - -

    Checkout

    - -
    - -
    Foo page
    -
    -
    - ); -}; diff --git a/exercises/exercise-8/src/pages/contact.page.js b/exercises/exercise-8/src/pages/contact.page.js deleted file mode 100644 index 1e2e74b..0000000 --- a/exercises/exercise-8/src/pages/contact.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-8/src/pages/home.page.js b/exercises/exercise-8/src/pages/home.page.js deleted file mode 100644 index 9ecdf58..0000000 --- a/exercises/exercise-8/src/pages/home.page.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-8/src/pages/login.page.js b/exercises/exercise-8/src/pages/login.page.js deleted file mode 100644 index b02f2d2..0000000 --- a/exercises/exercise-8/src/pages/login.page.js +++ /dev/null @@ -1,22 +0,0 @@ -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'; -import { Login } from '../modules/user/components/login.component'; - -export const LoginPage = () => { - return ( - - -

    Login

    - -
    - -
    - ); -}; diff --git a/exercises/exercise-9/README.md b/exercises/exercise-9/README.md deleted file mode 100644 index 6c86c41..0000000 --- a/exercises/exercise-9/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# 9/ Controlled Forms - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | ------------- | -| Modify | src/modules/checkout/checkout.component.js | {Checkout} | -| Modify | src/modules/checkout/components/review.component.js | {Review} | -| Modify | src/modules/checkout/components/addressForm.component.js | {AddressForm} | -| Modify | src/modules/checkout/components/paymentForm.component.js | {PaymentForm} | - -## TL;DR - -Let's create the controlled forms ! - -The **Stepper** gives a nice UX for combined forms however, outside _Material-UI_ code sample, we need to control every inputs and store their values in order to display it in the **Review** component. -Where would you locate the state then, in the forms' parent **Checkout** ? -What other options do you have ? How would you reduce the re-renders ? - -## Step by step - -### src/modules/checkout/checkout.component.js - -### src/modules/checkout/components/review.component.js - -### src/modules/checkout/components/addressForm.component.js - -### src/modules/checkout/components/paymentForm.component.js diff --git a/exercises/exercise-9/src/App.js b/exercises/exercise-9/src/App.js deleted file mode 100644 index 5abb118..0000000 --- a/exercises/exercise-9/src/App.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-9/src/__hints__/dialog.js b/exercises/exercise-9/src/__hints__/dialog.js deleted file mode 100644 index 079a0bc..0000000 --- a/exercises/exercise-9/src/__hints__/dialog.js +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable react/display-name */ -/* eslint-disable react/no-children-prop */ -/* eslint-disable react/prop-types */ - -import React from 'react'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import { makeStyles, useTheme } from '@material-ui/styles'; -import ReactMarkdown from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import gfm from 'remark-gfm'; -import instructionsMd from './instructions.md'; -import { lighten } from '@material-ui/core/styles/colorManipulator'; -import emoji from 'emoji-dictionary'; - -const emojiSupport = text => - text.value.replace(/:\w+:/gi, name => emoji.getUnicode(name) || name); - -const renderers = { - text: emojiSupport, - code: ({ language, value }) => { - return ( - - ); - }, -}; - -const useStyles = makeStyles(theme => ({ - position: { - position: 'fixed', - bottom: 10, - right: 10, - }, - dialog: { - maxHeight: 'unset', - padding: '2em', - - ['& pre > code']: { - background: 'inherit', - }, - ['& code']: { - background: 'lightgrey', - }, - ['& blockquote']: { - boxShadow: - '0px 3px 3px -2px rgb(0 0 0 / 20%), 0px 3px 4px 0px rgb(0 0 0 / 14%), 0px 1px 8px 0px rgb(0 0 0 / 12%)', - borderRadius: '4px', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', - padding: '20px', - borderLeft: '5px solid #3f51b5', - }, - - ['& p']: { - lineHeight: '25px', - }, - }, - table: { - ['& table']: { - display: 'table', - width: '100%', - borderCollapse: 'collapse', - borderSpacing: 0, - - '& tr': { - color: 'inherit', - display: 'table-row', - verticalAlign: 'middle', - // We disable the focus ring for mouse, touch and keyboard users. - outline: 0, - '&$hover:hover': { - backgroundColor: theme.palette.action.hover, - }, - '&$selected, &$selected:hover': { - backgroundColor: 'rgba(255,255,255,0.8)', - }, - }, - - ['& th, td']: { - ...theme.typography.body2, - fontSize: '15px', - display: 'table-cell', - verticalAlign: 'inherit', - // Workaround for a rendering bug with spanned columns in Chrome 62.0. - // Removes the alpha (sets it to 1), and lightens or darkens the theme color. - borderBottom: `1px solid rgba(0,0,0,0.25)`, - textAlign: 'left', - padding: theme.spacing(2), - }, - '& th': { - fontWeight: 'bold', - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastText, - }, - ['& tr:nth-child(2n+1)']: { - backgroundColor: lighten(theme.palette.primary.light, 0.9), - }, - }, - }, -})); - -export function SeeHints() { - const [open, setOpen] = React.useState(false); - const [markdownFile, setMarkdownFile] = React.useState(''); - - const theme = useTheme(); - - const classes = useStyles(theme); - - React.useEffect(() => { - fetch(instructionsMd) - .then(res => res.text()) - .then(setMarkdownFile) - .catch(console.error); - }, []); - - const handleClickOpen = () => { - setOpen(true); - }; - - const handleClose = () => { - setOpen(false); - }; - - return ( -
    - - - - -
    - ); -} - -export const ExerciseContainer = ({ children }) => ( - <> - {children} - - -); diff --git a/exercises/exercise-9/src/__hints__/instructions.md b/exercises/exercise-9/src/__hints__/instructions.md deleted file mode 100644 index 6c86c41..0000000 --- a/exercises/exercise-9/src/__hints__/instructions.md +++ /dev/null @@ -1,26 +0,0 @@ -# 9/ Controlled Forms - -| Action | Files | Exports | -| ------ | -------------------------------------------------------- | ------------- | -| Modify | src/modules/checkout/checkout.component.js | {Checkout} | -| Modify | src/modules/checkout/components/review.component.js | {Review} | -| Modify | src/modules/checkout/components/addressForm.component.js | {AddressForm} | -| Modify | src/modules/checkout/components/paymentForm.component.js | {PaymentForm} | - -## TL;DR - -Let's create the controlled forms ! - -The **Stepper** gives a nice UX for combined forms however, outside _Material-UI_ code sample, we need to control every inputs and store their values in order to display it in the **Review** component. -Where would you locate the state then, in the forms' parent **Checkout** ? -What other options do you have ? How would you reduce the re-renders ? - -## Step by step - -### src/modules/checkout/checkout.component.js - -### src/modules/checkout/components/review.component.js - -### src/modules/checkout/components/addressForm.component.js - -### src/modules/checkout/components/paymentForm.component.js diff --git a/exercises/exercise-9/src/__tests__/App.spec.js b/exercises/exercise-9/src/__tests__/App.spec.js deleted file mode 100644 index 34e1bc5..0000000 --- a/exercises/exercise-9/src/__tests__/App.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import App from '../App'; - -describe('App', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); -}); diff --git a/exercises/exercise-9/src/__tests__/__snapshots__/App.spec.js.snap b/exercises/exercise-9/src/__tests__/__snapshots__/App.spec.js.snap deleted file mode 100644 index f130870..0000000 --- a/exercises/exercise-9/src/__tests__/__snapshots__/App.spec.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`App should render correctly 1`] = ` - - - - - -`; diff --git a/exercises/exercise-9/src/components/__tests__/__snapshots__/layout.component.spec.js.snap b/exercises/exercise-9/src/components/__tests__/__snapshots__/layout.component.spec.js.snap deleted file mode 100644 index 3de464d..0000000 --- a/exercises/exercise-9/src/components/__tests__/__snapshots__/layout.component.spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - -

    - foo -

    -
    -
    -`; diff --git a/exercises/exercise-9/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap b/exercises/exercise-9/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap deleted file mode 100644 index 7495a9b..0000000 --- a/exercises/exercise-9/src/components/__tests__/__snapshots__/navbar.component.spec.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - Shopping App - - - - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    -`; diff --git a/exercises/exercise-9/src/components/__tests__/layout.component.spec.js b/exercises/exercise-9/src/components/__tests__/layout.component.spec.js deleted file mode 100644 index 6add5d5..0000000 --- a/exercises/exercise-9/src/components/__tests__/layout.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Layout } from '../layout.component'; - -let wrapper; - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('NavBar').exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-9/src/components/__tests__/navbar.component.spec.js b/exercises/exercise-9/src/components/__tests__/navbar.component.spec.js deleted file mode 100644 index f03d956..0000000 --- a/exercises/exercise-9/src/components/__tests__/navbar.component.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AppBar, IconButton, Menu, Typography } from '@material-ui/core'; - -import NavBar from '../navbar.component'; - -let wrapper; -jest.mock('../../modules/user/user.context.js', () => ({ - useUserState: jest.fn().mockReturnValue({ user: null }), - useUser: jest.fn().mockReturnValue([{ user: null }, jest.fn()]), -})); -const getWrapper = () => shallow(); - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(AppBar).exists()).toBeTruthy(); - expect(wrapper.find(Typography).first().text()).toBe('Shopping App'); - }); - }); - - describe('Interaction checks', () => { - it('should sur anchorEl from clicked item', () => { - expect(wrapper.find(Menu).prop('open')).toBeFalsy(); - wrapper.find(IconButton).last().prop('onClick')({ currentTarget: 'foo' }); - wrapper.update(); - expect(wrapper.find(Menu).prop('open')).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-9/src/components/gridTextField.component.js b/exercises/exercise-9/src/components/gridTextField.component.js deleted file mode 100644 index aab6c41..0000000 --- a/exercises/exercise-9/src/components/gridTextField.component.js +++ /dev/null @@ -1,37 +0,0 @@ -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/exercises/exercise-9/src/components/layout.component.js b/exercises/exercise-9/src/components/layout.component.js deleted file mode 100644 index d8bede0..0000000 --- a/exercises/exercise-9/src/components/layout.component.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-9/src/components/navbar.component.js b/exercises/exercise-9/src/components/navbar.component.js deleted file mode 100644 index 860eb70..0000000 --- a/exercises/exercise-9/src/components/navbar.component.js +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { Link } 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 { isUserConnected } from '../modules/user/user.selectors'; -import { useUser } from '../modules/user/user.context'; -import { login, 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 handleMenu = event => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const logInAndOut = () => { - dispatch(isConnected ? logout() : login()); - }; - - return ( - - - - Shopping App - - - - -
    - - - - - - Home - - - Contact - - - About - - -
    -
    -
    - ); -} diff --git a/exercises/exercise-9/src/constants/proptypes.constants.js b/exercises/exercise-9/src/constants/proptypes.constants.js deleted file mode 100644 index bfe9d9a..0000000 --- a/exercises/exercise-9/src/constants/proptypes.constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -export const CHILDREN_PROP_TYPES = PropTypes.oneOfType([ - PropTypes.array.isRequired, - PropTypes.object, - PropTypes.element, -]).isRequired; diff --git a/exercises/exercise-9/src/hooks/useInput.hook.js b/exercises/exercise-9/src/hooks/useInput.hook.js deleted file mode 100644 index a6942d5..0000000 --- a/exercises/exercise-9/src/hooks/useInput.hook.js +++ /dev/null @@ -1,9 +0,0 @@ -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/exercises/exercise-9/src/hooks/useOnLeave.hook.js b/exercises/exercise-9/src/hooks/useOnLeave.hook.js deleted file mode 100644 index 9357e17..0000000 --- a/exercises/exercise-9/src/hooks/useOnLeave.hook.js +++ /dev/null @@ -1,19 +0,0 @@ -/* 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/exercises/exercise-9/src/hooks/useStepperForm.hook.js b/exercises/exercise-9/src/hooks/useStepperForm.hook.js deleted file mode 100644 index 5ffeafa..0000000 --- a/exercises/exercise-9/src/hooks/useStepperForm.hook.js +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-9/src/hooks/useStepperFormChild.hook.js b/exercises/exercise-9/src/hooks/useStepperFormChild.hook.js deleted file mode 100644 index 7c713b5..0000000 --- a/exercises/exercise-9/src/hooks/useStepperFormChild.hook.js +++ /dev/null @@ -1,22 +0,0 @@ -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/exercises/exercise-9/src/index.css b/exercises/exercise-9/src/index.css deleted file mode 100644 index ec2585e..0000000 --- a/exercises/exercise-9/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -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/exercises/exercise-9/src/index.js b/exercises/exercise-9/src/index.js deleted file mode 100644 index 813c470..0000000 --- a/exercises/exercise-9/src/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import './index.css'; -import App from './App'; -import { ExerciseContainer } from './__hints__/dialog'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core'; - -const theme = createMuiTheme({ - spacing: n => n * 4, -}); - -ReactDOM.render( - - - - - , - document.getElementById('root'), -); diff --git a/exercises/exercise-9/src/logo.svg b/exercises/exercise-9/src/logo.svg deleted file mode 100644 index 2e5df0d..0000000 --- a/exercises/exercise-9/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/exercises/exercise-9/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap b/exercises/exercise-9/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap deleted file mode 100644 index 5a12ea9..0000000 --- a/exercises/exercise-9/src/modules/articles/__tests__/__snapshots__/articles.context.spec.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`articles.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-9/src/modules/articles/__tests__/articles.actions.spec.js b/exercises/exercise-9/src/modules/articles/__tests__/articles.actions.spec.js deleted file mode 100644 index 3c97a72..0000000 --- a/exercises/exercise-9/src/modules/articles/__tests__/articles.actions.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import { RECEIVED_ARTICLES, requestArticles } from '../articles.actions'; - -jest.mock('../../../utils/api.utils', () => ({ - 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/exercises/exercise-9/src/modules/articles/__tests__/articles.context.spec.js b/exercises/exercise-9/src/modules/articles/__tests__/articles.context.spec.js deleted file mode 100644 index fd72859..0000000 --- a/exercises/exercise-9/src/modules/articles/__tests__/articles.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useArticles, - useArticlesState, - useArticlesDispatch, - ArticlesProvider, -} from '../articles.context'; - -describe('articles.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set to the articles state articles', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toMatchObject({ articles: [] }); - }); - }); - - 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/exercises/exercise-9/src/modules/articles/__tests__/articles.reducer.spec.js b/exercises/exercise-9/src/modules/articles/__tests__/articles.reducer.spec.js deleted file mode 100644 index 4a9dfc1..0000000 --- a/exercises/exercise-9/src/modules/articles/__tests__/articles.reducer.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/articles.actions.js b/exercises/exercise-9/src/modules/articles/articles.actions.js deleted file mode 100644 index f4cc0e3..0000000 --- a/exercises/exercise-9/src/modules/articles/articles.actions.js +++ /dev/null @@ -1,9 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/articles.context.js b/exercises/exercise-9/src/modules/articles/articles.context.js deleted file mode 100644 index 3f16199..0000000 --- a/exercises/exercise-9/src/modules/articles/articles.context.js +++ /dev/null @@ -1,52 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/articles.reducer.js b/exercises/exercise-9/src/modules/articles/articles.reducer.js deleted file mode 100644 index b6518e5..0000000 --- a/exercises/exercise-9/src/modules/articles/articles.reducer.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/articles.selectors.js b/exercises/exercise-9/src/modules/articles/articles.selectors.js deleted file mode 100644 index a5ec396..0000000 --- a/exercises/exercise-9/src/modules/articles/articles.selectors.js +++ /dev/null @@ -1,10 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap b/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap deleted file mode 100644 index c3be7f5..0000000 --- a/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/article.component.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`
    Snapshot should render correctly 1`] = ` - -`; diff --git a/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap b/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap deleted file mode 100644 index 755f8de..0000000 --- a/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/articleCard.component.spec.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - bar - -
    - - 1 - - - 1 - $ - -
    -
    - - - Add to Cart - - - See more - - -
    -
    -`; diff --git a/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap b/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap deleted file mode 100644 index 09117d2..0000000 --- a/exercises/exercise-9/src/modules/articles/components/__tests__/__snapshots__/articlesList.component.spec.js.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - - - - -`; diff --git a/exercises/exercise-9/src/modules/articles/components/__tests__/article.component.spec.js b/exercises/exercise-9/src/modules/articles/components/__tests__/article.component.spec.js deleted file mode 100644 index 3c381e8..0000000 --- a/exercises/exercise-9/src/modules/articles/components/__tests__/article.component.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Article } from '../article.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = id => shallow(
    ); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('
    ', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(getWrapper(MockedArticles[0].slug)).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect( - getWrapper(MockedArticles[0].slug).find('ArticleCard').prop('article'), - ).toMatchObject(MockedArticles[0]); - }); - - it('should be null when no id is passed', () => { - expect(getWrapper('foo')).toBeEmptyRender(); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/articles/components/__tests__/articleCard.component.spec.js b/exercises/exercise-9/src/modules/articles/components/__tests__/articleCard.component.spec.js deleted file mode 100644 index 1cf0a94..0000000 --- a/exercises/exercise-9/src/modules/articles/components/__tests__/articleCard.component.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticleCard } from '../articleCard.component'; - -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 { useCart } from '../../../cart/cart.context'; - -jest.mock('../../../cart/cart.context.js', () => ({ - useCart: jest.fn().mockReturnValue([null, jest.fn()]), -})); -let wrapper; - -const article = { - id: 'abc', - slug: 'foo', - name: 'bar', - year: '1', - image: 'baz', - price: 1, -}; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(CardMedia).prop('image')).toBe(article.image); - expect(wrapper.find(CardContent).find(Typography).first().text()).toBe( - article.name, - ); - expect( - wrapper.find(CardContent).find('div').find(Typography).first().text(), - ).toBe(article.year); - expect( - wrapper.find(CardContent).find('div').find(Typography).last().text(), - ).toBe(`${article.price} $`); - expect(wrapper.find(CardActions).find(Button).first().text()).toBe( - 'Add to Cart', - ); - expect(wrapper.find(CardActions).find(Button).last().text()).toBe( - 'See more', - ); - }); - }); - - describe('life cycles', () => { - it('should call useCart', () => { - expect(useCart).toHaveBeenCalled(); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/articles/components/__tests__/articlesList.component.spec.js b/exercises/exercise-9/src/modules/articles/components/__tests__/articlesList.component.spec.js deleted file mode 100644 index 1b62497..0000000 --- a/exercises/exercise-9/src/modules/articles/components/__tests__/articlesList.component.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Grid } from '@material-ui/core'; - -import { ArticlesList } from '../articlesList.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import * as selector from '../../articles.selectors'; - -let wrapper; - -jest.mock('../../articles.selectors'); -selector.useArticlesSelector = jest.fn().mockReturnValue(MockedArticles); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Grid).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(Grid) - .children() - .map(node => node.prop('article')), - ).toMatchObject(MockedArticles); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/articles/components/article.component.js b/exercises/exercise-9/src/modules/articles/components/article.component.js deleted file mode 100644 index 4ac07c4..0000000 --- a/exercises/exercise-9/src/modules/articles/components/article.component.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/components/articleCard.component.js b/exercises/exercise-9/src/modules/articles/components/articleCard.component.js deleted file mode 100644 index 3f47f09..0000000 --- a/exercises/exercise-9/src/modules/articles/components/articleCard.component.js +++ /dev/null @@ -1,88 +0,0 @@ -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/exercises/exercise-9/src/modules/articles/components/articlesList.component.js b/exercises/exercise-9/src/modules/articles/components/articlesList.component.js deleted file mode 100644 index 5fa4989..0000000 --- a/exercises/exercise-9/src/modules/articles/components/articlesList.component.js +++ /dev/null @@ -1,19 +0,0 @@ -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/exercises/exercise-9/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap b/exercises/exercise-9/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap deleted file mode 100644 index 90bc395..0000000 --- a/exercises/exercise-9/src/modules/cart/__tests__/__snapshots__/cart.context.spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`cart.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-9/src/modules/cart/__tests__/cart.actions.spec.js b/exercises/exercise-9/src/modules/cart/__tests__/cart.actions.spec.js deleted file mode 100644 index 56b470f..0000000 --- a/exercises/exercise-9/src/modules/cart/__tests__/cart.actions.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -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/exercises/exercise-9/src/modules/cart/__tests__/cart.context.spec.js b/exercises/exercise-9/src/modules/cart/__tests__/cart.context.spec.js deleted file mode 100644 index 1bac696..0000000 --- a/exercises/exercise-9/src/modules/cart/__tests__/cart.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useCart, - useCartState, - useCartDispatch, - CartProvider, -} from '../cart.context'; - -describe('cart.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set to the cart state cart', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toEqual({ articles: {}, total: 0 }); - }); - }); - - 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/exercises/exercise-9/src/modules/cart/__tests__/cart.reducer.spec.js b/exercises/exercise-9/src/modules/cart/__tests__/cart.reducer.spec.js deleted file mode 100644 index 95726f4..0000000 --- a/exercises/exercise-9/src/modules/cart/__tests__/cart.reducer.spec.js +++ /dev/null @@ -1,107 +0,0 @@ -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/exercises/exercise-9/src/modules/cart/cart.actions.js b/exercises/exercise-9/src/modules/cart/cart.actions.js deleted file mode 100644 index e7239d9..0000000 --- a/exercises/exercise-9/src/modules/cart/cart.actions.js +++ /dev/null @@ -1,6 +0,0 @@ -export const ADD_TO_CART = 'cart/ADD_TO_CART'; -export const REMOVE_FROM_CART = 'cart/REMOVE_FROM_CART'; - -export const addToCart = article => ({ type: ADD_TO_CART, article }); - -export const removeFromCart = id => ({ type: REMOVE_FROM_CART, id }); diff --git a/exercises/exercise-9/src/modules/cart/cart.context.js b/exercises/exercise-9/src/modules/cart/cart.context.js deleted file mode 100644 index 5aa097c..0000000 --- a/exercises/exercise-9/src/modules/cart/cart.context.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; - -import { cartReducer, initialState } from './cart.reducer'; - -import { dispatchThunk } from '../../utils/context.utils'; -import { CHILDREN_PROP_TYPES } from '../../constants/proptypes.constants'; - -const CartStateContext = React.createContext(); -const CartDispatchContext = React.createContext(); - -const CartProvider = ({ children }) => { - const [state, dispatch] = React.useReducer(cartReducer, initialState); - const getState = React.useCallback(() => state, [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/exercises/exercise-9/src/modules/cart/cart.reducer.js b/exercises/exercise-9/src/modules/cart/cart.reducer.js deleted file mode 100644 index dabcb26..0000000 --- a/exercises/exercise-9/src/modules/cart/cart.reducer.js +++ /dev/null @@ -1,75 +0,0 @@ -import { ADD_TO_CART, REMOVE_FROM_CART } from './cart.actions'; - -export const initialState = { - articles: {}, - total: 0, -}; - -export const cartReducer = (state, action) => { - switch (action.type) { - 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/exercises/exercise-9/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap b/exercises/exercise-9/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap deleted file mode 100644 index b622b4f..0000000 --- a/exercises/exercise-9/src/modules/cart/components/__tests__/__snapshots__/cart.component.spec.js.snap +++ /dev/null @@ -1,300 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly with articles 1`] = ` - - - - Cart - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Total Price: - $ - - - - - Check out - - - -`; diff --git a/exercises/exercise-9/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap b/exercises/exercise-9/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap deleted file mode 100644 index 23eb4a7..0000000 --- a/exercises/exercise-9/src/modules/cart/components/__tests__/__snapshots__/cartLayout.component.spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly with articles 1`] = ` - - -

    - foo -

    -
    - - - -
    -`; diff --git a/exercises/exercise-9/src/modules/cart/components/__tests__/cart.component.spec.js b/exercises/exercise-9/src/modules/cart/components/__tests__/cart.component.spec.js deleted file mode 100644 index 0db4032..0000000 --- a/exercises/exercise-9/src/modules/cart/components/__tests__/cart.component.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Cart } from '../cart.component'; -import MockedArticles from '../../../../../../fixtures/articles.json'; -import { useCart } from '../../cart.context'; -import { Card, CardContent, List, Typography } from '@material-ui/core'; - -jest.mock('../../cart.context', () => ({ - useCart: jest.fn().mockReturnValue([{ articles: {} }, jest.fn()]), -})); - -const getWrapper = () => shallow(); - -const articles = MockedArticles.reduce( - (acc, curr) => ({ ...acc, [curr.id]: curr }), - {}, -); - -describe('', () => { - let wrapper; - beforeEach(() => { - jest.clearAllMocks(); - useCart.mockReturnValue([{ articles }, jest.fn()]); - wrapper = getWrapper(); - }); - - describe('Snapshot', () => { - it('should render correctly with articles', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find(Card).exists()).toBeTruthy(); - expect(wrapper.find(Typography).exists()).toBeTruthy(); - expect(wrapper.find(CardContent).exists()).toBeTruthy(); - expect(wrapper.find(List).exists()).toBeTruthy(); - }); - - it('should contain the correct list markup', () => { - expect(wrapper.find(List).children()).toHaveLength(MockedArticles.length); - - return expect( - wrapper - .find(List) - .children() - .map(node => node.prop('to')), - ).toEqual( - MockedArticles.reduce( - (acc, curr) => [...acc, `/articles/${curr.slug}`], - [], - ), - ); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/cart/components/__tests__/cartLayout.component.spec.js b/exercises/exercise-9/src/modules/cart/components/__tests__/cartLayout.component.spec.js deleted file mode 100644 index c069907..0000000 --- a/exercises/exercise-9/src/modules/cart/components/__tests__/cartLayout.component.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { CartLayout } from '../cartLayout.component'; -import { Grid } from '@material-ui/core'; - -jest.mock('../../cart.context', () => ({ - useCart: jest.fn().mockReturnValue([{ articles: {} }, jest.fn()]), -})); - -const getWrapper = () => - shallow( - -

    foo

    -
    , - ); - -describe('', () => { - let wrapper; - beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); - }); - - describe('Snapshot', () => { - it('should render correctly with articles', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Cart').exists()).toBeTruthy(); - expect(wrapper.find(Grid).exists()).toBeTruthy(); - expect(wrapper.find('p').text()).toBe('foo'); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/cart/components/cart.component.js b/exercises/exercise-9/src/modules/cart/components/cart.component.js deleted file mode 100644 index 8ad6d65..0000000 --- a/exercises/exercise-9/src/modules/cart/components/cart.component.js +++ /dev/null @@ -1,99 +0,0 @@ -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 => ( - - - - - - - - - - ))} - - - Total Price: {total} $ - - - - - - - ); -} diff --git a/exercises/exercise-9/src/modules/cart/components/cartLayout.component.js b/exercises/exercise-9/src/modules/cart/components/cartLayout.component.js deleted file mode 100644 index af37943..0000000 --- a/exercises/exercise-9/src/modules/cart/components/cartLayout.component.js +++ /dev/null @@ -1,22 +0,0 @@ -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/exercises/exercise-9/src/modules/checkout/checkout.component.js b/exercises/exercise-9/src/modules/checkout/checkout.component.js deleted file mode 100644 index 98fd12a..0000000 --- a/exercises/exercise-9/src/modules/checkout/checkout.component.js +++ /dev/null @@ -1,150 +0,0 @@ -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/exercises/exercise-9/src/modules/checkout/checkout.constants.js b/exercises/exercise-9/src/modules/checkout/checkout.constants.js deleted file mode 100644 index c1959d0..0000000 --- a/exercises/exercise-9/src/modules/checkout/checkout.constants.js +++ /dev/null @@ -1,5 +0,0 @@ -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/exercises/exercise-9/src/modules/checkout/components/addressForm.component.js b/exercises/exercise-9/src/modules/checkout/components/addressForm.component.js deleted file mode 100644 index 42becfe..0000000 --- a/exercises/exercise-9/src/modules/checkout/components/addressForm.component.js +++ /dev/null @@ -1,78 +0,0 @@ -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/exercises/exercise-9/src/modules/checkout/components/paymentForm.component.js b/exercises/exercise-9/src/modules/checkout/components/paymentForm.component.js deleted file mode 100644 index 25aa7bc..0000000 --- a/exercises/exercise-9/src/modules/checkout/components/paymentForm.component.js +++ /dev/null @@ -1,68 +0,0 @@ -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/exercises/exercise-9/src/modules/checkout/components/review.component.js b/exercises/exercise-9/src/modules/checkout/components/review.component.js deleted file mode 100644 index 48f9372..0000000 --- a/exercises/exercise-9/src/modules/checkout/components/review.component.js +++ /dev/null @@ -1,118 +0,0 @@ -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/exercises/exercise-9/src/modules/routing/__tests__/rooting.hooks.spec.js b/exercises/exercise-9/src/modules/routing/__tests__/rooting.hooks.spec.js deleted file mode 100644 index 2c01a79..0000000 --- a/exercises/exercise-9/src/modules/routing/__tests__/rooting.hooks.spec.js +++ /dev/null @@ -1,151 +0,0 @@ -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 { shallow } from 'enzyme'; - -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); - }); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/routing/components/__tests__/__snapshots__/routes.component.spec.js.snap b/exercises/exercise-9/src/modules/routing/components/__tests__/__snapshots__/routes.component.spec.js.snap deleted file mode 100644 index 22561c6..0000000 --- a/exercises/exercise-9/src/modules/routing/components/__tests__/__snapshots__/routes.component.spec.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - -`; diff --git a/exercises/exercise-9/src/modules/routing/components/__tests__/routes.component.spec.js b/exercises/exercise-9/src/modules/routing/components/__tests__/routes.component.spec.js deleted file mode 100644 index 2f75c75..0000000 --- a/exercises/exercise-9/src/modules/routing/components/__tests__/routes.component.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; -import { AppRoutes } from '../routes.component'; -import { ROUTES_PATHS_BY_NAMES } from '../../routing.constants'; -import { Route } from 'react-router-dom'; - -jest.mock('../../routing.hooks.js', () => ({ useLoginRedirect: jest.fn() })); - -let wrapper; - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(shallow()).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - beforeEach(() => { - wrapper = shallow(); - }); - - it('should have routes for all paths', () => { - expect( - wrapper - .find(Route) - .map(node => node.prop('path')) - .sort(), - ).toMatchObject(Object.values(ROUTES_PATHS_BY_NAMES).sort()); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/routing/components/routes.component.js b/exercises/exercise-9/src/modules/routing/components/routes.component.js deleted file mode 100644 index 7aef6e0..0000000 --- a/exercises/exercise-9/src/modules/routing/components/routes.component.js +++ /dev/null @@ -1,50 +0,0 @@ -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/exercises/exercise-9/src/modules/routing/routing.constants.js b/exercises/exercise-9/src/modules/routing/routing.constants.js deleted file mode 100644 index b5e0e63..0000000 --- a/exercises/exercise-9/src/modules/routing/routing.constants.js +++ /dev/null @@ -1,10 +0,0 @@ -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/exercises/exercise-9/src/modules/routing/routing.hooks.js b/exercises/exercise-9/src/modules/routing/routing.hooks.js deleted file mode 100644 index d47402b..0000000 --- a/exercises/exercise-9/src/modules/routing/routing.hooks.js +++ /dev/null @@ -1,35 +0,0 @@ -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/exercises/exercise-9/src/modules/user/__tests__/__snapshots__/user.context.spec.js.snap b/exercises/exercise-9/src/modules/user/__tests__/__snapshots__/user.context.spec.js.snap deleted file mode 100644 index 041c67a..0000000 --- a/exercises/exercise-9/src/modules/user/__tests__/__snapshots__/user.context.spec.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`user.context should render nested providers 1`] = ` - - -
    - - -`; diff --git a/exercises/exercise-9/src/modules/user/__tests__/user.actions.spec.js b/exercises/exercise-9/src/modules/user/__tests__/user.actions.spec.js deleted file mode 100644 index c5971be..0000000 --- a/exercises/exercise-9/src/modules/user/__tests__/user.actions.spec.js +++ /dev/null @@ -1,45 +0,0 @@ -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/exercises/exercise-9/src/modules/user/__tests__/user.context.spec.js b/exercises/exercise-9/src/modules/user/__tests__/user.context.spec.js deleted file mode 100644 index cc5b5e5..0000000 --- a/exercises/exercise-9/src/modules/user/__tests__/user.context.spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { - useUser, - useUserState, - useUserDispatch, - UserProvider, -} from '../user.context'; - -describe('user.context', () => { - describe('', () => { - it('should render nested providers', () => { - expect( - shallow( - -
    - , - ), - ).toMatchSnapshot(); - }); - - it('should have property value set', () => { - expect( - shallow( - -
    - , - ) - .find('ContextProvider') - .first() - .prop('value'), - ).toMatchObject({ user: null }); - }); - }); - - 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/exercises/exercise-9/src/modules/user/__tests__/user.reducer.spec.js b/exercises/exercise-9/src/modules/user/__tests__/user.reducer.spec.js deleted file mode 100644 index 3a23a5d..0000000 --- a/exercises/exercise-9/src/modules/user/__tests__/user.reducer.spec.js +++ /dev/null @@ -1,29 +0,0 @@ -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/exercises/exercise-9/src/modules/user/__tests__/user.selectors.spec.js b/exercises/exercise-9/src/modules/user/__tests__/user.selectors.spec.js deleted file mode 100644 index 5a96502..0000000 --- a/exercises/exercise-9/src/modules/user/__tests__/user.selectors.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -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/exercises/exercise-9/src/modules/user/components/__tests__/__snapshots__/login.component.spec.js.snap b/exercises/exercise-9/src/modules/user/components/__tests__/__snapshots__/login.component.spec.js.snap deleted file mode 100644 index b7709d8..0000000 --- a/exercises/exercise-9/src/modules/user/components/__tests__/__snapshots__/login.component.spec.js.snap +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -
    - - - - - Sign in - -
    - - - - } - label="Remember me" - /> - - Sign In - - - - - Forgot password? - - - - - Don't have an account? Sign Up - - - - -
    -
    -`; diff --git a/exercises/exercise-9/src/modules/user/components/__tests__/login.component.spec.js b/exercises/exercise-9/src/modules/user/components/__tests__/login.component.spec.js deleted file mode 100644 index d5adb6c..0000000 --- a/exercises/exercise-9/src/modules/user/components/__tests__/login.component.spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { Login } from '../login.component'; -import { useInput } from '../../../../hooks/useInput.hook'; -import { Button, TextField } from '@material-ui/core'; -import { login } from '../../user.actions'; - -jest.mock('../../user.context.js', () => ({ - useUserDispatch: jest - .fn() - .mockReturnValue(args => (typeof args === 'function' ? args() : args)), -})); - -jest.mock('../../user.actions.js', () => ({ login: jest.fn() })); - -jest.mock('../../../../hooks/useInput.hook.js', () => ({ - useInput: jest.fn().mockReturnValue(['', jest.fn()]), -})); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - wrapper = shallow(); - }); - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should render a Button', () => { - expect(wrapper.find(Button).text()).toBe('Sign In'); - }); - - describe('TextFields', () => { - it('should render a first TextField', () => { - expect(wrapper.find(TextField).first().prop('name')).toBe('email'); - }); - it('should render a second TextField', () => { - expect(wrapper.find(TextField).last().prop('name')).toBe('password'); - }); - }); - }); - - describe('Hooks', () => { - it('should call useInput twice', () => { - expect(useInput).toHaveBeenCalled(); - }); - }); - - describe('Interaction checks', () => { - it('should dispatch login on submit', () => { - shallow() - .find('form') - .invoke('onSubmit')({ preventDefault: jest.fn() }); - - expect(login).toBeCalled(); - }); - }); -}); diff --git a/exercises/exercise-9/src/modules/user/components/login.component.js b/exercises/exercise-9/src/modules/user/components/login.component.js deleted file mode 100644 index 0f2a1a7..0000000 --- a/exercises/exercise-9/src/modules/user/components/login.component.js +++ /dev/null @@ -1,116 +0,0 @@ -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/exercises/exercise-9/src/modules/user/user.actions.js b/exercises/exercise-9/src/modules/user/user.actions.js deleted file mode 100644 index 4089a18..0000000 --- a/exercises/exercise-9/src/modules/user/user.actions.js +++ /dev/null @@ -1,35 +0,0 @@ -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/exercises/exercise-9/src/modules/user/user.context.js b/exercises/exercise-9/src/modules/user/user.context.js deleted file mode 100644 index de6b9bf..0000000 --- a/exercises/exercise-9/src/modules/user/user.context.js +++ /dev/null @@ -1,54 +0,0 @@ -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/exercises/exercise-9/src/modules/user/user.hooks.js b/exercises/exercise-9/src/modules/user/user.hooks.js deleted file mode 100644 index ba4cad3..0000000 --- a/exercises/exercise-9/src/modules/user/user.hooks.js +++ /dev/null @@ -1,5 +0,0 @@ -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/exercises/exercise-9/src/modules/user/user.reducer.js b/exercises/exercise-9/src/modules/user/user.reducer.js deleted file mode 100644 index bb50cc5..0000000 --- a/exercises/exercise-9/src/modules/user/user.reducer.js +++ /dev/null @@ -1,25 +0,0 @@ -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/exercises/exercise-9/src/modules/user/user.selectors.js b/exercises/exercise-9/src/modules/user/user.selectors.js deleted file mode 100644 index 4d798c1..0000000 --- a/exercises/exercise-9/src/modules/user/user.selectors.js +++ /dev/null @@ -1,2 +0,0 @@ -export const isUserConnected = ({ user }) => !!user; -export const getUser = ({ user }) => user; diff --git a/exercises/exercise-9/src/pages/__tests__/__snapshots__/about.page.spec.js.snap b/exercises/exercise-9/src/pages/__tests__/__snapshots__/about.page.spec.js.snap deleted file mode 100644 index 71bc0e1..0000000 --- a/exercises/exercise-9/src/pages/__tests__/__snapshots__/about.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - About -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-9/src/pages/__tests__/__snapshots__/article.page.spec.js.snap b/exercises/exercise-9/src/pages/__tests__/__snapshots__/article.page.spec.js.snap deleted file mode 100644 index e75a78e..0000000 --- a/exercises/exercise-9/src/pages/__tests__/__snapshots__/article.page.spec.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - - -

    - Article - foo -

    - - Return to Home - -
    - -
    - - -`; diff --git a/exercises/exercise-9/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap b/exercises/exercise-9/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap deleted file mode 100644 index 8c0408c..0000000 --- a/exercises/exercise-9/src/pages/__tests__/__snapshots__/contact.page.spec.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render correctly 1`] = ` - - -

    - Contact -

    - - Return to Home - -
    -
    -`; diff --git a/exercises/exercise-9/src/pages/__tests__/__snapshots__/home.page.spec.js.snap b/exercises/exercise-9/src/pages/__tests__/__snapshots__/home.page.spec.js.snap deleted file mode 100644 index ef599e5..0000000 --- a/exercises/exercise-9/src/pages/__tests__/__snapshots__/home.page.spec.js.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Snapshot should render correctly 1`] = ` - -

    - Home Page -

    - - - -
    -`; diff --git a/exercises/exercise-9/src/pages/__tests__/about.page.spec.js b/exercises/exercise-9/src/pages/__tests__/about.page.spec.js deleted file mode 100644 index 6d7eb91..0000000 --- a/exercises/exercise-9/src/pages/__tests__/about.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { AboutPage } from '../about.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-9/src/pages/__tests__/article.page.spec.js b/exercises/exercise-9/src/pages/__tests__/article.page.spec.js deleted file mode 100644 index a32b00b..0000000 --- a/exercises/exercise-9/src/pages/__tests__/article.page.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ArticlePage } from '../article.page'; - -let wrapper; - -jest.mock('react-router-dom', () => ({ - useParams: jest.fn().mockReturnValue({ id: 'foo' }), -})); - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`Article`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-9/src/pages/__tests__/contact.page.spec.js b/exercises/exercise-9/src/pages/__tests__/contact.page.spec.js deleted file mode 100644 index 0e29df1..0000000 --- a/exercises/exercise-9/src/pages/__tests__/contact.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { ContactPage } from '../contact.page'; -import { Link } from 'react-router-dom'; -import { Button } from '@material-ui/core'; - -const getWrapper = () => shallow(); - -let wrapper; - -describe('', () => { - beforeEach(() => { - jest.clearAllMocks(); - - wrapper = getWrapper(); - }); - - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - - it('should contain Button as Link', () => { - expect(wrapper.find(Button).prop('component')).toBe(Link); - expect(wrapper.find(Button).prop('to')).toBe('/'); - }); - - it('should contain h2', () => { - expect(wrapper.find('h2').exists()).toBeTruthy(); - }); -}); diff --git a/exercises/exercise-9/src/pages/__tests__/home.page.spec.js b/exercises/exercise-9/src/pages/__tests__/home.page.spec.js deleted file mode 100644 index 11eb6d9..0000000 --- a/exercises/exercise-9/src/pages/__tests__/home.page.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; - -import { shallow } from 'enzyme'; - -import { HomePage } from '../home.page'; - -let wrapper; - -const getWrapper = () => shallow(); - -beforeEach(() => { - jest.clearAllMocks(); - wrapper = getWrapper(); -}); - -describe('', () => { - describe('Snapshot', () => { - it('should render correctly', () => { - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('Markup checks', () => { - it('should contain the correct markup', () => { - expect(wrapper.find('Layout').exists()).toBeTruthy(); - expect(wrapper.find(`[data-testid='app-title']`).text()).toBe( - 'Home Page', - ); - expect(wrapper.find(`ArticlesList`).exists()).toBeTruthy(); - }); - }); -}); diff --git a/exercises/exercise-9/src/pages/about.page.js b/exercises/exercise-9/src/pages/about.page.js deleted file mode 100644 index 567e4b9..0000000 --- a/exercises/exercise-9/src/pages/about.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-9/src/pages/article.page.js b/exercises/exercise-9/src/pages/article.page.js deleted file mode 100644 index 09193c7..0000000 --- a/exercises/exercise-9/src/pages/article.page.js +++ /dev/null @@ -1,27 +0,0 @@ -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/exercises/exercise-9/src/pages/checkout.page.js b/exercises/exercise-9/src/pages/checkout.page.js deleted file mode 100644 index a510d80..0000000 --- a/exercises/exercise-9/src/pages/checkout.page.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -import { Layout } from '../components/layout.component'; -import Checkout from '../modules/checkout/checkout.component'; - -export const CheckoutPage = () => { - return ( - - - - ); -}; diff --git a/exercises/exercise-9/src/pages/contact.page.js b/exercises/exercise-9/src/pages/contact.page.js deleted file mode 100644 index 1e2e74b..0000000 --- a/exercises/exercise-9/src/pages/contact.page.js +++ /dev/null @@ -1,20 +0,0 @@ -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/exercises/exercise-9/src/pages/home.page.js b/exercises/exercise-9/src/pages/home.page.js deleted file mode 100644 index 9ecdf58..0000000 --- a/exercises/exercise-9/src/pages/home.page.js +++ /dev/null @@ -1,16 +0,0 @@ -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/exercises/exercise-9/src/pages/login.page.js b/exercises/exercise-9/src/pages/login.page.js deleted file mode 100644 index 97c2092..0000000 --- a/exercises/exercise-9/src/pages/login.page.js +++ /dev/null @@ -1,12 +0,0 @@ -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/exercises/exercise-9/src/utils/context.utils.js b/exercises/exercise-9/src/utils/context.utils.js deleted file mode 100644 index 8998f4c..0000000 --- a/exercises/exercise-9/src/utils/context.utils.js +++ /dev/null @@ -1,33 +0,0 @@ -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; -};