├── .babelrc ├── .eslintrc ├── .gitignore ├── .nvmrc ├── README.md ├── actions ├── actionTypes.js └── index.js ├── components ├── App.js ├── ExerciseOne.js ├── ExerciseTwo.js └── ExerciseZero.js ├── containers ├── IntelligentExerciseOne.js ├── IntelligentExerciseTwo.js └── IntelligentExerciseZero.js ├── index.html ├── index.js ├── npm-shrinkwrap.json ├── package.json ├── readme ├── extension.png ├── redux1.jpg ├── redux2.png └── redux3.png ├── reducers ├── exercise0.js ├── exercise1.js ├── exercise2.js └── index.js ├── server.js ├── solutions ├── exercise1 ├── exercise2 └── exercise3 ├── store └── configureStore.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "import" 6 | ], 7 | "rules": { 8 | "indent": [2, 4], 9 | "max-len": [1, 120, 4, {"ignoreUrls": true}], 10 | "id-length": [1, {"min": 2, "exceptions": ["x", "y", "e", "i", "j", "k", "d", "n", "_", "$"]}], 11 | "object-shorthand": [2, "methods"], 12 | "arrow-body-style": [0], 13 | "no-new": [1], 14 | "no-multi-spaces": [0], 15 | "no-warning-comments": [1, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 16 | "react/sort-comp": [0], 17 | "react/jsx-boolean-value": [0], 18 | "react/jsx-no-bind": [0], 19 | "react/prefer-es6-class": [0, "never"], 20 | "react/jsx-indent-props": [2, 4], 21 | "react/jsx-indent": [2, 4], 22 | "jsx-quotes": [1, "prefer-double"], 23 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }] 24 | }, 25 | 26 | "env": { 27 | "es6": true, 28 | "browser": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.9 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-redux-exercise 2 | 3 | > Exercise to understand better React and Redux, how they work and how to use them. 4 | 5 | **If you like this repo and/or learnt something from it, please give us a star :) Thanks!** 6 | 7 | ## Get started 8 | 9 | ### Prerequisites 10 | 11 | This project uses [nvm](https://github.com/creationix/nvm). 12 | 13 | You need to have it installed on your machine. 14 | 15 | ### Install 16 | 17 | To clone the project on your machine, install the required dependencies and run the server, follow these commands: 18 | 19 | ```sh 20 | $ git clone https://github.com/springload/react-redux-exercise.git 21 | 22 | $ cd react-redux-exercise 23 | 24 | # Install the correct version of Node/NPM with nvm 25 | $ nvm install 26 | 27 | # Then, install all project dependencies. 28 | $ npm install 29 | 30 | # Then run the server 31 | $ npm run start 32 | 33 | # Open your browser to http://localhost:3000 34 | ``` 35 | 36 | And if you want some `eslint` love (and you should): 37 | 38 | `npm run lint .` (or specify the path to the file you want to check) 39 | 40 | ### Debugging 41 | 42 | I highly recommend that you install the [Redux extension for Chrome](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en) 43 | 44 | It looks like this: 45 | 46 | ![alt Extension image](./readme/extension.png) 47 | 48 | And it will help you a lot to debug and understand what's going with your code. 49 | 50 | ## Redux 51 | 52 | ### with images 53 | 54 | ![alt Redux image](./readme/redux3.png) 55 | 56 | Source https://www.reddit.com/r/webdev/comments/4r1947/i_am_working_on_a_reactredux_video_tutorial/ 57 | 58 | ![alt Redux image](./readme/redux1.jpg) 59 | 60 | Source http://staltz.com/unidirectional-user-interface-architectures.html 61 | 62 | ![alt Redux image](./readme/redux2.png) 63 | 64 | Source http://blog.tighten.co/react-101-using-redux 65 | 66 | 67 | ### with words 68 | 69 | Don't be afraid by these images if you find them complicated. 70 | Redux just needs to be tested and you will pick it up. 71 | 72 | Basically, a Redux cycle works like this: 73 | - A user clicks on a button on the UI (for instance) 74 | - This button dispatches an action 75 | - This action will be managed by a reducer which is listening for one or many actions 76 | - This reducer will update the store state 77 | - The new store is then passed to the component which rerenders with the new value 78 | -------------------------------------------------------------------------------- /actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'keymirror'; 2 | 3 | export default keyMirror({ 4 | CHANGE_VALUE: null, 5 | BOX_TICKED: null, 6 | }); 7 | -------------------------------------------------------------------------------- /actions/index.js: -------------------------------------------------------------------------------- 1 | // Ok, let's say the user started typing something in the input of ExerciseZero 2 | // The onClick function is triggered. 3 | // This one comes from IntelligentExerciseZero 4 | // and it dispatches an action 5 | // This action (changeValue) is defined here 6 | // Meet me in ../reducers/exercise0.js once you've understood this file 7 | import actionTypes from './actionTypes'; 8 | 9 | // This is a function that creates an action 10 | // it get some data (here an event) 11 | // and then returns an object with a type (mandatory) 12 | // and some other params which will be used inside the reducer 13 | // It's following the FSA convention: https://github.com/acdlite/flux-standard-action#flux-standard-action 14 | export const changeValue = (event) => { 15 | return { 16 | type: actionTypes.CHANGE_VALUE, 17 | payload: { 18 | newValue: event.target.value, 19 | }, 20 | }; 21 | }; 22 | 23 | // TODO: IMPLEMENT ME 24 | // I work with /reducers/exercise1.js 25 | export const buttonClicked = () => { 26 | }; 27 | 28 | export const boxTicked = (event) => { 29 | return { 30 | type: actionTypes.BOX_TICKED, 31 | payload: { 32 | hasTickedBox: event.target.checked, 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import IntelligentExerciseZero from '../containers/IntelligentExerciseZero'; 4 | import IntelligentExerciseOne from '../containers/IntelligentExerciseOne'; 5 | import IntelligentExerciseTwo from '../containers/IntelligentExerciseTwo'; 6 | 7 | // This is where you build the skeleton of your App 8 | // by displaying the right intelligent components 9 | // Meet me in ../components/ExerciseZero after you've understood this file 10 | const App = () => ( 11 |
12 | 13 | 14 | 15 | 16 |

Exercise 3

17 | 18 |
19 | 20 | Build me from scratch!
21 | I need to be a select field which will display the new selected value
22 | BONUS POINT: and the previous selected value ;)
23 | Select values are ['', 'blue', 'white', 'red' 24 | ] - default value = '' 25 |
26 |
27 |
28 | ); 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /components/ExerciseOne.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | name: PropTypes.string.isRequired, 5 | buttonClicked: PropTypes.func.isRequired, 6 | }; 7 | 8 | // This component should stay "dumb" (i.e. stateless) 9 | // It should not have it's own state and should only use props 10 | const ExerciseOne = ({ name, buttonClicked }) => ( 11 |
12 |

Exercise 1

13 |
Instructions: I want to know which button (use its name) has been clicked.
14 |
TODO: Implement the action for this Redux cycle
15 |
{name} just got clicked!
16 |
17 | 18 | 19 |
20 |
21 | ); 22 | 23 | ExerciseOne.propTypes = propTypes; 24 | 25 | export default ExerciseOne; 26 | -------------------------------------------------------------------------------- /components/ExerciseTwo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | hasTickedBox: PropTypes.bool.isRequired, 5 | boxTicked: PropTypes.func.isRequired, 6 | }; 7 | 8 | // This component should stay "dumb" (i.e. stateless) 9 | // It should not have it's own state and should only use props 10 | const ExerciseTwo = ({ hasTickedBox, boxTicked }) => ( 11 |
12 |

Exercise 2

13 |
Instructions: I want to know if the checkbox has been ticked or not.
14 |
TODO: Implement the reducer for this Redux cycle
15 |
16 | {hasTickedBox === true ? 'Kewl!' : 'I need you to tick this box'} 17 |
18 |
19 | 29 |
30 |
31 | ); 32 | 33 | ExerciseTwo.propTypes = propTypes; 34 | 35 | export default ExerciseTwo; 36 | -------------------------------------------------------------------------------- /components/ExerciseZero.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | // it's considered better practice to specify into 4 | // the component all the props you are expecting. 5 | // Other developers then know what they can provide or need to provide. 6 | // I prefer to declare them at the beginning of the file 7 | // so once you open the component you know directly what it needs to work. 8 | const propTypes = { 9 | value: PropTypes.string.isRequired, 10 | changeValue: PropTypes.func.isRequired, 11 | }; 12 | 13 | // This is what's wrapped into an "intelligent" component. 14 | // This is a "dumb" component: it just displays data and calls functions, 15 | // oblivious to the fact that Redux is used on the project. 16 | // It's basically just displaying HTML and using the given props. 17 | // The syntax is using ES6. To learn more about React Stateless components in ES6, 18 | // please check this link: https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc#.tw7p0usxv 19 | // Meet me in ../containers/IntelligentExerciseZero.js once you've understood this file 20 | const ExerciseZero = ({ value, changeValue }) => ( 21 |
22 |

Exercise 0

23 |
24 | 25 | Use this exercise as a reference for a working example. 26 | Follow me through the file comments.
27 | Meet me in /index.js 28 |
29 |
30 |
Value: {value}
31 |
32 | 42 |
43 |
44 | ); 45 | 46 | ExerciseZero.propTypes = propTypes; 47 | 48 | export default ExerciseZero; 49 | -------------------------------------------------------------------------------- /containers/IntelligentExerciseOne.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ExerciseOne from '../components/ExerciseOne'; 3 | import * as actions from '../actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | name: state.exercise1.name, 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | buttonClicked: (event) => { 14 | dispatch(actions.buttonClicked(event)); 15 | }, 16 | }; 17 | }; 18 | 19 | const IntelligentExerciseOne = connect( 20 | mapStateToProps, 21 | mapDispatchToProps, 22 | )(ExerciseOne); 23 | 24 | export default IntelligentExerciseOne; 25 | -------------------------------------------------------------------------------- /containers/IntelligentExerciseTwo.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ExerciseTwo from '../components/ExerciseTwo'; 3 | import * as actions from '../actions'; 4 | 5 | const mapStateToProps = (state) => { 6 | return { 7 | hasTickedBox: state.exercise2.hasTickedBox, 8 | }; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | boxTicked: (event) => { 14 | dispatch(actions.boxTicked(event)); 15 | }, 16 | }; 17 | }; 18 | 19 | const IntelligentExerciseTwo = connect( 20 | mapStateToProps, 21 | mapDispatchToProps, 22 | )(ExerciseTwo); 23 | 24 | export default IntelligentExerciseTwo; 25 | -------------------------------------------------------------------------------- /containers/IntelligentExerciseZero.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import ExerciseZero from '../components/ExerciseZero'; 3 | import * as actions from '../actions'; 4 | 5 | // This is an "intelligent" component which is taking a "dumb" rendering component 6 | // and provides some Redux intelligence using the props. 7 | // Meet me in ../actions/index.js after you've understood this file. 8 | 9 | // Here are the values we want to take from the redux store's state, 10 | // and provide to our component as props. 11 | // We _map_ the store's state to a component's props. mapStateToProps. 12 | const mapStateToProps = (state) => { 13 | return { 14 | // in this case we want the value for exercise0 15 | value: state.exercise0.value, 16 | }; 17 | }; 18 | 19 | // Here are the methods we want to give to the component. 20 | // These methods dispatch (send to the store) actions, 21 | // that will then be processed by Redux reducers 22 | // (functions that receive an action and change the store data accordingly). 23 | // We _map_ actions (well, dispatched actions) to a component's props. 24 | const mapDispatchToProps = (dispatch) => { 25 | return { 26 | changeValue: (event) => { 27 | dispatch(actions.changeValue(event)); 28 | }, 29 | }; 30 | }; 31 | 32 | // This is creating a component based on ExerciseZero but 33 | // adding the vars above into its props. 34 | // connect is a redux function that creates the intermediary "intelligent" React component 35 | // around the "dumb" React component. 36 | const IntelligentExerciseZero = connect( 37 | mapStateToProps, 38 | mapDispatchToProps, 39 | )(ExerciseZero); 40 | 41 | export default IntelligentExerciseZero; 42 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux exercises 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import App from './components/App'; 6 | import store from './store/configureStore'; 7 | 8 | // This is the "root" of your React App 9 | // The render function bootstraps React onto the page, in a specific element (#root). 10 | // A Redux Provider component is wrapping to provide access to the Redux store. 11 | // Meet me in /components/App.js 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-exercise", 3 | "version": "1.0.0", 4 | "description": "React redux exercise", 5 | "scripts": { 6 | "start": "node server.js", 7 | "lint": "eslint" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/springload/react-redux-exercise.git" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/springload/react-redux-exercise/issues" 16 | }, 17 | "author": "Vincent Audebert ", 18 | "contributors": [ 19 | { 20 | "name": "Thibaud Colas" 21 | } 22 | ], 23 | "homepage": "https://github.com/springload/react-redux-exercise", 24 | "dependencies": { 25 | "babel-polyfill": "^6.3.14", 26 | "express": "^4.13.3", 27 | "keymirror": "^0.1.1", 28 | "react": "^15.4.2", 29 | "react-dom": "^15.4.2", 30 | "react-redux": "^5.0.2", 31 | "redux": "^3.6.0", 32 | "redux-thunk": "^2.2.0", 33 | "webpack": "^1.14.0", 34 | "webpack-dev-middleware": "^1.2.0" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^6.3.15", 38 | "babel-loader": "^6.2.0", 39 | "babel-preset-es2015": "^6.3.13", 40 | "babel-preset-react": "^6.3.13", 41 | "babel-preset-react-hmre": "^1.1.1", 42 | "babel-register": "^6.3.13", 43 | "cross-env": "^1.0.7", 44 | "eslint": "https://registry.npmjs.org/eslint/-/eslint-3.14.1.tgz", 45 | "eslint-config-airbnb": "^14.0.0", 46 | "eslint-plugin-import": "^2.2.0", 47 | "eslint-plugin-jsx-a11y": "^3.0.2", 48 | "eslint-plugin-react": "^6.9.0", 49 | "expect": "^1.8.0", 50 | "jsdom": "^5.6.1", 51 | "node-libs-browser": "^0.5.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /readme/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/react-redux-exercise/cec2b123b97a2dc9ede78f4a50af764a7644a751/readme/extension.png -------------------------------------------------------------------------------- /readme/redux1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/react-redux-exercise/cec2b123b97a2dc9ede78f4a50af764a7644a751/readme/redux1.jpg -------------------------------------------------------------------------------- /readme/redux2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/react-redux-exercise/cec2b123b97a2dc9ede78f4a50af764a7644a751/readme/redux2.png -------------------------------------------------------------------------------- /readme/redux3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/springload/react-redux-exercise/cec2b123b97a2dc9ede78f4a50af764a7644a751/readme/redux3.png -------------------------------------------------------------------------------- /reducers/exercise0.js: -------------------------------------------------------------------------------- 1 | // Here is one of the reducers. 2 | // A reducer is a function that receives actions and changes the store state accordingly. 3 | // It's "listening" for some actions to happen once a `dispatch` occurred. 4 | // In this case this reducer is listening for "CHANGE_VALUE" action. 5 | // But it could listen for many more actions. 6 | 7 | // When an action occures, it changes the store's state to reflect what that action conveys. 8 | // The store state needs to be immutable: a new state is re-created from the old one, hence why we use `assign`. 9 | // Reducers ALWAYS return the state (new or unchanged). 10 | 11 | // An application can be composed of multiple reducers, each of them acting on a separate part of the overall state. 12 | // The global application state within the Redux store can be created by 13 | // combining the sub-states retuned by all of the reducers. 14 | // Here, this reducer works on the "exercise0" part of the state, available 15 | // in `state.exercise0.whatevervalue` 16 | // Get started on Exercise 1 once you've understood this file :) 17 | 18 | import actionTypes from '../actions/actionTypes'; 19 | 20 | // Each reducer must define the initial state it works on. 21 | const initialState = { 22 | value: 'Initial value', 23 | }; 24 | 25 | const changeValue = (state, action) => { 26 | return Object.assign({}, state, { 27 | value: action.payload.newValue, 28 | }); 29 | }; 30 | 31 | const exercise0 = (state = initialState, action) => { 32 | // usually reducer core is just a switch on action.type 33 | // if you need to perform operations on values, create an external function and use it 34 | switch (action.type) { 35 | case actionTypes.CHANGE_VALUE: 36 | return changeValue(state, action); 37 | default: 38 | return state; 39 | } 40 | }; 41 | 42 | export default exercise0; 43 | -------------------------------------------------------------------------------- /reducers/exercise1.js: -------------------------------------------------------------------------------- 1 | import actionTypes from '../actions/actionTypes'; 2 | 3 | const initialState = { 4 | name: '', 5 | }; 6 | 7 | const buttonClicked = (state, action) => { 8 | return Object.assign({}, state, { 9 | name: action.payload.buttonWhoGotClickedName, 10 | }); 11 | }; 12 | 13 | const exercise = (state = initialState, action) => { 14 | switch (action.type) { 15 | case actionTypes.BUTTON_CLICKED: 16 | return buttonClicked(state, action); 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default exercise; 23 | -------------------------------------------------------------------------------- /reducers/exercise2.js: -------------------------------------------------------------------------------- 1 | // TODO: Implement me 2 | // I work with /actions/index.js -> boxTicked 3 | const initialState = { 4 | hasTickedBox: false, 5 | }; 6 | 7 | const exercise = (state = initialState, action) => { 8 | switch (action.type) { 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default exercise; 15 | -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import exercise0 from './exercise0'; 3 | import exercise1 from './exercise1'; 4 | import exercise2 from './exercise2'; 5 | // import exercise3 from './exercise3' 6 | 7 | // this is combining all the reducers we have in the app 8 | // you can access each of them using state.exercise0, state.exercise1, etc... 9 | const rootReducer = combineReducers({ 10 | exercise0, 11 | exercise1, 12 | exercise2, 13 | // exercise3, 14 | }); 15 | 16 | export default rootReducer; 17 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, global-require */ 2 | 3 | const webpack = require('webpack'); 4 | const webpackDevMiddleware = require('webpack-dev-middleware'); 5 | const config = require('./webpack.config'); 6 | 7 | const app = new (require('express'))(); 8 | 9 | const port = 3000; 10 | 11 | const compiler = webpack(config); 12 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })); 13 | 14 | app.get('/', (req, res) => { 15 | res.sendFile(`${__dirname}/index.html`); 16 | }); 17 | 18 | app.listen(port, (error) => { 19 | if (error) { 20 | console.error(error); 21 | } else { 22 | console.info('==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /solutions/exercise1: -------------------------------------------------------------------------------- 1 | PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovYWN0aW9ucy9hY3Rpb25UeXBlcy5qcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQppbXBvcnQga2V5TWlycm9yIGZyb20gJ2tleW1pcnJvcic7DQoNCmV4cG9ydCBkZWZhdWx0IGtleU1pcnJvcih7DQogICAgQ0hBTkdFX1ZBTFVFOiBudWxsLA0KICAgIEJPWF9USUNLRUQ6IG51bGwsDQogICAgQlVUVE9OX0NMSUNLRUQ6IG51bGwsDQp9KTsNCg0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovYWN0aW9ucy9pbmRleC5qcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovLyBUT0RPOiBJTVBMRU1FTlQgTUUNCi8vIEkgd29yayB3aXRoIC9yZWR1Y2Vycy9leGVyY2lzZTEuanMNCmV4cG9ydCBjb25zdCBidXR0b25DbGlja2VkID0gKGV2ZW50KSA9PiB7DQogICAgcmV0dXJuIHsNCiAgICAgICAgdHlwZTogYWN0aW9uVHlwZXMuQlVUVE9OX0NMSUNLRUQsDQogICAgICAgIHBheWxvYWQ6IHsNCiAgICAgICAgICAgIGJ1dHRvbldob0dvdENsaWNrZWROYW1lOiBldmVudC50YXJnZXQubmFtZSwNCiAgICAgICAgfSwNCiAgICB9Ow0KfTsNCg== -------------------------------------------------------------------------------- /solutions/exercise2: -------------------------------------------------------------------------------- 1 | PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovcmVkdWNlcnMvZXhlcmNpc2UyLmpzDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCi8vIFRPRE86IEltcGxlbWVudCBtZQ0KLy8gSSB3b3JrIHdpdGggL2FjdGlvbnMvaW5kZXguanMgLT4gYm94VGlja2VkDQppbXBvcnQgYWN0aW9uVHlwZXMgZnJvbSAnLi4vYWN0aW9ucy9hY3Rpb25UeXBlcyc7DQoNCmNvbnN0IGluaXRpYWxTdGF0ZSA9IHsNCiAgICBoYXNUaWNrZWRCb3g6IGZhbHNlLA0KfTsNCg0KY29uc3QgYm94VGlja2VkID0gKHN0YXRlLCBhY3Rpb24pID0+IHsNCiAgICByZXR1cm4gT2JqZWN0LmFzc2lnbih7fSwgc3RhdGUsIHsNCiAgICAgICAgaGFzVGlja2VkQm94OiAhYWN0aW9uLnBheWxvYWQuaGFzVGlja2VkQm94LA0KICAgIH0pOw0KfTsNCg0KY29uc3QgZXhlcmNpc2UgPSAoc3RhdGUgPSBpbml0aWFsU3RhdGUsIGFjdGlvbikgPT4gew0KICAgIHN3aXRjaCAoYWN0aW9uLnR5cGUpIHsNCiAgICBjYXNlIGFjdGlvblR5cGVzLkJPWF9USUNLRUQ6DQogICAgICAgIHJldHVybiBib3hUaWNrZWQoc3RhdGUsIGFjdGlvbik7DQogICAgZGVmYXVsdDoNCiAgICAgICAgcmV0dXJuIHN0YXRlOw0KICAgIH0NCn07DQoNCmV4cG9ydCBkZWZhdWx0IGV4ZXJjaXNlOw0K -------------------------------------------------------------------------------- /solutions/exercise3: -------------------------------------------------------------------------------- 1 | PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovYWN0aW9ucy9hY3Rpb25zVHlwZXMuanMNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KaW1wb3J0IGtleU1pcnJvciBmcm9tICdrZXltaXJyb3InOw0KDQpleHBvcnQgZGVmYXVsdCBrZXlNaXJyb3Ioew0KICAgIENIQU5HRV9WQUxVRTogbnVsbCwNCiAgICBCT1hfVElDS0VEOiBudWxsLA0KICAgIFZBTFVFX1NFTEVDVEVEOiBudWxsLA0KfSk7DQoNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KL2FjdGlvbnMvaW5kZXguanMNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KWy4uLl0NCg0KLy8gZXhlcmNpc2UzDQpleHBvcnQgY29uc3QgdmFsdWVTZWxlY3RlZCA9IChldmVudCkgPT4gew0KICAgIHJldHVybiB7DQogICAgICAgIHR5cGU6IGFjdGlvblR5cGVzLlZBTFVFX1NFTEVDVEVELA0KICAgICAgICBwYXlsb2FkOiB7DQogICAgICAgICAgICBuZXdWYWx1ZTogZXZlbnQudGFyZ2V0LnZhbHVlLA0KICAgICAgICB9LA0KICAgIH07DQp9Ow0KDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCi9jb21wb25lbnRzL0FwcC5qcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQppbXBvcnQgUmVhY3QgZnJvbSAncmVhY3QnOw0KDQppbXBvcnQgSW50ZWxsaWdlbnRFeGVyY2lzZVplcm8gZnJvbSAnLi4vY29udGFpbmVycy9JbnRlbGxpZ2VudEV4ZXJjaXNlWmVybyc7DQppbXBvcnQgSW50ZWxsaWdlbnRFeGVyY2lzZU9uZSBmcm9tICcuLi9jb250YWluZXJzL0ludGVsbGlnZW50RXhlcmNpc2VPbmUnOw0KaW1wb3J0IEludGVsbGlnZW50RXhlcmNpc2VUd28gZnJvbSAnLi4vY29udGFpbmVycy9JbnRlbGxpZ2VudEV4ZXJjaXNlVHdvJzsNCmltcG9ydCBJbnRlbGxpZ2VudEV4ZXJjaXNlVGhyZWUgZnJvbSAnLi4vY29udGFpbmVycy9JbnRlbGxpZ2VudEV4ZXJjaXNlVGhyZWUnOw0KDQovLyBUaGlzIGlzIHdoZXJlIHlvdSBidWlsZCB0aGUgc2tlbGV0b24gb2YgeW91ciBBcHANCi8vIGJ5IGRpc3BsYXlpbmcgdGhlIHJpZ2h0IGludGVsbGlnZW50IGNvbXBvbmVudHMNCi8vIE1lZXQgbWUgaW4gLi4vY29tcG9uZW50cy9FeGVyY2lzZVplcm8gYWZ0ZXIgeW91J3ZlIHVuZGVyc3Rvb2QgdGhpcyBmaWxlDQpjb25zdCBBcHAgPSAoKSA9PiAoDQogICAgPGRpdj4NCiAgICAgICAgPEludGVsbGlnZW50RXhlcmNpc2VaZXJvIC8+DQogICAgICAgIDxJbnRlbGxpZ2VudEV4ZXJjaXNlT25lIC8+DQogICAgICAgIDxJbnRlbGxpZ2VudEV4ZXJjaXNlVHdvIC8+DQogICAgICAgIDxJbnRlbGxpZ2VudEV4ZXJjaXNlVGhyZWUgLz4NCiAgICA8L2Rpdj4NCik7DQoNCmV4cG9ydCBkZWZhdWx0IEFwcDsNCg0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQovY29tcG9uZW50cy9FeGVyY2lzZVRocmVlLmpzDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCmltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCc7DQoNCmNvbnN0IHByb3BUeXBlcyA9IHsNCiAgICBuZXdWYWx1ZTogUmVhY3QuUHJvcFR5cGVzLnN0cmluZy5pc1JlcXVpcmVkLA0KICAgIG9sZFZhbHVlOiBSZWFjdC5Qcm9wVHlwZXMuc3RyaW5nLmlzUmVxdWlyZWQsDQogICAgdmFsdWVTZWxlY3RlZDogUmVhY3QuUHJvcFR5cGVzLmZ1bmMuaXNSZXF1aXJlZCwNCn07DQoNCi8vIFRoaXMgY29tcG9uZW50IHNob3VsZCBzdGF5ICJkdW1iIiAoaS5lLiBzdGF0ZWxlc3MpDQovLyBJdCBzaG91bGQgbm90IGhhdmUgaXQncyBvd24gc3RhdGUgYW5kIHNob3VsZCBvbmx5IHVzZSBwcm9wcw0KY29uc3QgRXhlcmNpc2VUaHJlZSA9ICh7IG5ld1ZhbHVlLCBvbGRWYWx1ZSwgdmFsdWVTZWxlY3RlZCB9KSA9PiAoDQogICAgPGRpdj4NCiAgICAgICAgPGgxPkV4ZXJjaXNlIDM8L2gxPg0KICAgICAgICA8ZGl2IHN0eWxlPXt7IG1hcmdpbkJvdHRvbTogJzVweCcgfX0+T2xkIHZhbHVlOiB7b2xkVmFsdWV9PC9kaXY+DQogICAgICAgIDxkaXYgc3R5bGU9e3sgbWFyZ2luQm90dG9tOiAnNXB4JyB9fT5OZXcgdmFsdWU6IHtuZXdWYWx1ZX08L2Rpdj4NCiAgICAgICAgPGRpdj4NCiAgICAgICAgICAgIDxsYWJlbCBodG1sRm9yPSJzZWxlY3RGaWVsZCI+DQogICAgICAgICAgICAgICAgPHNlbGVjdCBpZD0ic2VsZWN0RmllbGQiIG5hbWU9InNlbGVjdCIgdmFsdWU9e25ld1ZhbHVlfSBvbkNoYW5nZT17dmFsdWVTZWxlY3RlZH0+DQogICAgICAgICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9IiI+U2VsZWN0IG1lPC9vcHRpb24+DQogICAgICAgICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9ImJsdWUiPkJsdWU8L29wdGlvbj4NCiAgICAgICAgICAgICAgICAgICAgPG9wdGlvbiB2YWx1ZT0id2hpdGUiPldoaXRlPC9vcHRpb24+DQogICAgICAgICAgICAgICAgICAgIDxvcHRpb24gdmFsdWU9InJlZCI+UmVkPC9vcHRpb24+DQogICAgICAgICAgICAgICAgPC9zZWxlY3Q+DQogICAgICAgICAgICA8L2xhYmVsPg0KICAgICAgICA8L2Rpdj4NCiAgICA8L2Rpdj4NCik7DQoNCkV4ZXJjaXNlVGhyZWUucHJvcFR5cGVzID0gcHJvcFR5cGVzOw0KDQpleHBvcnQgZGVmYXVsdCBFeGVyY2lzZVRocmVlOw0KDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCi9jb21wb25lbnRzL0ludGVsbGlnZW50RXhlcmNpc2VUaHJlZS5qcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQppbXBvcnQgeyBjb25uZWN0IH0gZnJvbSAncmVhY3QtcmVkdXgnOw0KaW1wb3J0IEV4ZXJjaXNlVGhyZWUgZnJvbSAnLi4vY29tcG9uZW50cy9FeGVyY2lzZVRocmVlJzsNCmltcG9ydCAqIGFzIGFjdGlvbnMgZnJvbSAnLi4vYWN0aW9ucyc7DQoNCmNvbnN0IG1hcFN0YXRlVG9Qcm9wcyA9IChzdGF0ZSkgPT4gew0KICAgIHJldHVybiB7DQogICAgICAgIG9sZFZhbHVlOiBzdGF0ZS5leGVyY2lzZTMub2xkVmFsdWUsDQogICAgICAgIG5ld1ZhbHVlOiBzdGF0ZS5leGVyY2lzZTMubmV3VmFsdWUsDQogICAgfTsNCn07DQoNCmNvbnN0IG1hcERpc3BhdGNoVG9Qcm9wcyA9IChkaXNwYXRjaCkgPT4gew0KICAgIHJldHVybiB7DQogICAgICAgIHZhbHVlU2VsZWN0ZWQ6IChldmVudCkgPT4gew0KICAgICAgICAgICAgZGlzcGF0Y2goYWN0aW9ucy52YWx1ZVNlbGVjdGVkKGV2ZW50KSk7DQogICAgICAgIH0sDQogICAgfTsNCn07DQoNCmNvbnN0IEludGVsbGlnZW50RXhlcmNpc2VUaHJlZSA9IGNvbm5lY3QoDQogICAgbWFwU3RhdGVUb1Byb3BzLA0KICAgIG1hcERpc3BhdGNoVG9Qcm9wcywNCikoRXhlcmNpc2VUaHJlZSk7DQoNCmV4cG9ydCBkZWZhdWx0IEludGVsbGlnZW50RXhlcmNpc2VUaHJlZTsNCg0KDQo9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0NCi9yZWR1Y2Vycy9leGVyY2lzZTMuanMNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KaW1wb3J0IGFjdGlvblR5cGVzIGZyb20gJy4uL2FjdGlvbnMvYWN0aW9uVHlwZXMnOw0KDQpjb25zdCBpbml0aWFsU3RhdGUgPSB7DQogICAgb2xkVmFsdWU6ICcnLA0KICAgIG5ld1ZhbHVlOiAnJywNCn07DQoNCi8vIEp1c3QgbWFraW5nIHRoaXMgdG8gc2hvdyBob3cgaXQgd29ya3Mgd2l0aCBleHRlcm5hbCBmdW5jdGlvbnMNCi8vIGhlcmUgd2UganVzdCBhc3NpZ24gdmFycyBidXQgd2UgY291bGQgZG8gbW9yZSBjb21wbGV4IG9wZXJhdGlvbnMNCmNvbnN0IGNoYW5nZVZhbHVlcyA9IChzdGF0ZSwgYWN0aW9uKSA9PiB7DQogICAgY29uc3Qgb2xkVmFsdWUgPSBzdGF0ZS5uZXdWYWx1ZTsNCiAgICBjb25zdCBuZXdWYWx1ZSA9IGFjdGlvbi5wYXlsb2FkLm5ld1ZhbHVlOw0KICAgIHJldHVybiBPYmplY3QuYXNzaWduKHt9LCBzdGF0ZSwgew0KICAgICAgICBvbGRWYWx1ZTogb2xkVmFsdWUsDQogICAgICAgIG5ld1ZhbHVlOiBuZXdWYWx1ZSwNCiAgICB9KTsNCn07DQoNCmNvbnN0IGV4ZXJjaXNlID0gKHN0YXRlID0gaW5pdGlhbFN0YXRlLCBhY3Rpb24pID0+IHsNCiAgICBzd2l0Y2ggKGFjdGlvbi50eXBlKSB7DQogICAgY2FzZSBhY3Rpb25UeXBlcy5WQUxVRV9TRUxFQ1RFRDoNCiAgICAgICAgcmV0dXJuIGNoYW5nZVZhbHVlcyhzdGF0ZSwgYWN0aW9uKTsNCiAgICBkZWZhdWx0Og0KICAgICAgICByZXR1cm4gc3RhdGU7DQogICAgfQ0KfTsNCg0KZXhwb3J0IGRlZmF1bHQgZXhlcmNpc2U7DQoNCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KL3JlZHVjZXJzL2V4ZXJjaXNlMy5qcw0KPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQppbXBvcnQgeyBjb21iaW5lUmVkdWNlcnMgfSBmcm9tICdyZWR1eCc7DQppbXBvcnQgZXhlcmNpc2UwIGZyb20gJy4vZXhlcmNpc2UwJzsNCmltcG9ydCBleGVyY2lzZTEgZnJvbSAnLi9leGVyY2lzZTEnOw0KaW1wb3J0IGV4ZXJjaXNlMiBmcm9tICcuL2V4ZXJjaXNlMic7DQppbXBvcnQgZXhlcmNpc2UzIGZyb20gJy4vZXhlcmNpc2UzJzsNCg0KLy8gdGhpcyBpcyBjb21iaW5pbmcgYWxsIHRoZSByZWR1Y2VycyB3ZSBoYXZlIGluIHRoZSBhcHANCi8vIHlvdSBjYW4gYWNjZXNzIGVhY2ggb2YgdGhlbSB1c2luZyBzdGF0ZS5leGVyY2lzZTAsIHN0YXRlLmV4ZXJjaXNlMSwgZXRjLi4uDQpjb25zdCByb290UmVkdWNlciA9IGNvbWJpbmVSZWR1Y2Vycyh7DQogICAgZXhlcmNpc2UwLA0KICAgIGV4ZXJjaXNlMSwNCiAgICBleGVyY2lzZTIsDQogICAgZXhlcmNpc2UzLA0KfSk7DQoNCmV4cG9ydCBkZWZhdWx0IHJvb3RSZWR1Y2VyOw0K -------------------------------------------------------------------------------- /store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | const middleware = [ 6 | thunkMiddleware, 7 | ]; 8 | 9 | export default createStore(rootReducer, compose( 10 | applyMiddleware(...middleware), 11 | // Expose store to Redux DevTools extension. 12 | global.devToolsExtension ? global.devToolsExtension() : fct => fct, 13 | )); 14 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | './index', 7 | ], 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'bundle.js', 11 | publicPath: '/static/', 12 | }, 13 | plugins: [ 14 | new webpack.optimize.OccurenceOrderPlugin(), 15 | new webpack.SourceMapDevToolPlugin({ 16 | filename: '[file].map', 17 | columns: false, 18 | }), 19 | ], 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loaders: ['babel'], 25 | exclude: /node_modules/, 26 | include: __dirname, 27 | }, 28 | ], 29 | }, 30 | }; 31 | --------------------------------------------------------------------------------