├── .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 | 
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 | 
55 |
56 | Source https://www.reddit.com/r/webdev/comments/4r1947/i_am_working_on_a_reactredux_video_tutorial/
57 |
58 | 
59 |
60 | Source http://staltz.com/unidirectional-user-interface-architectures.html
61 |
62 | 
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 |
--------------------------------------------------------------------------------