├── .gitignore ├── README.md ├── package.json ├── public ├── favicon2.ico ├── index.html └── wanted_list.json └── src ├── actions ├── add_person.js ├── clear_toast.js ├── delete_person.js ├── get_wanted_list.js ├── new_toast.js ├── types.js └── update_person.js ├── components ├── AddUserModal.js ├── App.js ├── LoadingSpinner.js ├── NewUserFace.js ├── Note.js ├── RewardList.js ├── Toast.js └── WantedCard.js ├── index.js ├── reducers ├── index.js ├── reducer_toast.js └── reducer_wanted_list.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Tutorial: A Practical Guide to Redux 2 | 3 | This repo is the companion to my blog post, [A Practical Guide to Redux](http://lorenstewart.me/2016/11/27/a-practical-guide-to-redux/). 4 | 5 | [DEMO](http://lorenstewart.me/redux-wanted-list/) 6 | 7 | To get started: 8 | 1. Make sure you're using Node 6 or higher (4 and higher will work, though) 9 | 2. `npm install create-react-app -g` (if you don't have it installed already) 10 | 3. `npm install` 11 | 4. `npm run start` 12 | 5. Open [http://localhost:3000/](http://localhost:3000/) in your browser 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-checklist", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.7.0" 7 | }, 8 | "dependencies": { 9 | "axios": "^0.15.2", 10 | "react": "^15.3.2", 11 | "react-dom": "^15.3.2", 12 | "react-redux": "^4.4.5", 13 | "redux": "^3.6.0", 14 | "redux-thunk": "^2.1.0", 15 | "spectre.css": "^0.1.27" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/favicon2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorenseanstewart/redux-wanted-list/85919872df9a21e226aa630a808f62f12a949b7f/public/favicon2.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Redux Wanted List 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/wanted_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Butterfingers McGee", 4 | "image": "https://api.adorable.io/avatars/face/eyes3/nose2/mouth4/F5CDE6", 5 | "reason": "Butterfingers attempted to steal the world's most valuable faberge egg, but dropped it.", 6 | "reward": "$500,000" 7 | }, 8 | { 9 | "name": "Ravenous Reggie", 10 | "image": "https://api.adorable.io/avatars/face/eyes6/nose8/mouth7/CDCB8D", 11 | "reason": "This ravenous man eats his roommate's food without asking.", 12 | "reward": "A sincere handshake." 13 | }, 14 | { 15 | "name": "Jerry the Vandal", 16 | "image": "https://api.adorable.io/avatars/face/eyes2/nose7/mouth3/819143", 17 | "reason": "Jerry drives to construction areas all around town, and writes his name in the wet cement.", 18 | "reward": "An engraved plaque from the city council." 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /src/actions/add_person.js: -------------------------------------------------------------------------------- 1 | import { ADD_PERSON } from './types'; 2 | import newToast from './new_toast'; 3 | 4 | export default function addPerson(person) { 5 | const message = `You've just added ${person.name} to the Most Wanted List.`; 6 | return dispatch => { 7 | dispatch(addPersonAsync(person)); 8 | dispatch(newToast(message)) 9 | } 10 | } 11 | 12 | function addPersonAsync(person){ 13 | return { 14 | type: ADD_PERSON, 15 | payload: person 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/actions/clear_toast.js: -------------------------------------------------------------------------------- 1 | import { CLEAR_TOAST } from './types'; 2 | 3 | export default function clearToast() { 4 | return dispatch => { 5 | dispatch(clearToastAsync()); 6 | } 7 | } 8 | 9 | function clearToastAsync(){ 10 | return { 11 | type: CLEAR_TOAST, 12 | payload: null 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/delete_person.js: -------------------------------------------------------------------------------- 1 | import { DELETE_PERSON } from './types'; 2 | import newToast from './new_toast'; 3 | 4 | export default function deletePerson(person) { 5 | const message = `You've just captured ${person.name}. Go collect your reward!`; 6 | return dispatch => { 7 | dispatch(deletePersonAsync(person)); 8 | dispatch(newToast(message)) 9 | } 10 | } 11 | 12 | function deletePersonAsync(person){ 13 | return { 14 | type: DELETE_PERSON, 15 | payload: person // assuming every person has a unique name (which you should never do!), this will work. 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/actions/get_wanted_list.js: -------------------------------------------------------------------------------- 1 | import { GET_WANTED_LIST } from './types'; 2 | import axios from 'axios'; 3 | 4 | export default function getWantedList() { 5 | return dispatch => { 6 | axios.get('../wanted_list.json') 7 | .then(res => { 8 | console.log('Wanted list ::', res.data); 9 | const people = res.data.map(person => { 10 | person.note = 'none'; 11 | return person; 12 | }); 13 | dispatch(getWantedListAsync(people)); 14 | }); 15 | } 16 | } 17 | 18 | function getWantedListAsync(people){ 19 | return { 20 | type: GET_WANTED_LIST, 21 | payload: people 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/actions/new_toast.js: -------------------------------------------------------------------------------- 1 | import { NEW_TOAST } from './types'; 2 | 3 | export default function newToast(message) { 4 | return dispatch => { 5 | dispatch(newToastAsync(message)); 6 | } 7 | } 8 | 9 | function newToastAsync(message){ 10 | return { 11 | type: NEW_TOAST, 12 | payload: message 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const GET_WANTED_LIST = 'get_wanted_list'; 2 | export const ADD_PERSON = 'add_person_to_wanted_list'; 3 | export const UPDATE_PERSON = 'update_person'; 4 | export const DELETE_PERSON = 'delete_person_from_wanted_list'; 5 | export const NEW_TOAST = 'new_toast'; 6 | export const CLEAR_TOAST = 'clear_toast'; 7 | -------------------------------------------------------------------------------- /src/actions/update_person.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_PERSON } from './types'; 2 | 3 | export default function updatePerson(person) { 4 | return dispatch => { 5 | dispatch(updatePersonAsync(person)); 6 | } 7 | } 8 | 9 | function updatePersonAsync(person){ 10 | return { 11 | type: UPDATE_PERSON, 12 | payload: person 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/AddUserModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import NewUserFace from './NewUserFace'; 3 | 4 | class AddUserModal extends Component { 5 | render() { 6 | return ( 7 |
8 |
9 |
10 |
11 | 14 |
15 |

Add someone to the Most Wanted list

16 |
17 |
18 |
19 |
20 |
21 |
22 | 23 | 29 |
30 |
31 | 32 | 38 |
39 |
40 | 41 | 47 |
48 |
49 | 50 |
51 | 59 | 67 | 75 |
76 |
77 |
78 | 79 |
80 |
81 | 86 |
87 |
88 | 93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | 103 |
104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | export default AddUserModal; 111 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import addPerson from '../actions/add_person'; 5 | import getWantedList from '../actions/get_wanted_list'; 6 | import clearToast from '../actions/clear_toast'; 7 | import WantedCard from './WantedCard'; 8 | import RewardList from './RewardList'; 9 | import AddUserModal from './AddUserModal'; 10 | import Toast from './Toast'; 11 | import LoadingSpinner from './LoadingSpinner'; 12 | 13 | class App extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | openModal: false, 18 | newPersonName: '', 19 | newPersonReason: '', 20 | newPersonReward: '', 21 | newPersonEyes: '', 22 | newPersonNose: '', 23 | newPersonMouth: '', 24 | newPersonSkin: '#CE96FF' 25 | } 26 | this.toggleModalState = this.toggleModalState.bind(this); 27 | this.handleNewPersonNameChange = this.handleNewPersonNameChange.bind(this); 28 | this.handleNewPersonReasonChange = this.handleNewPersonReasonChange.bind(this); 29 | this.handleNewPersonRewardChange = this.handleNewPersonRewardChange.bind(this); 30 | this.handleNewPersonNoseChange = this.handleNewPersonNoseChange.bind(this); 31 | this.handleNewPersonMouthChange = this.handleNewPersonMouthChange.bind(this); 32 | this.handleNewPersonEyeChange = this.handleNewPersonEyeChange.bind(this); 33 | this.handleSkinChange = this.handleSkinChange.bind(this); 34 | this.handlePersonCreation = this.handlePersonCreation.bind(this); 35 | this.handleClearToast = this.handleClearToast.bind(this); 36 | } 37 | componentDidMount() { 38 | this.props.getWantedList(); 39 | } 40 | renderUsers() { 41 | if(this.props.wantedPeople) { 42 | return this.props.wantedPeople.map(person => { 43 | return ; 44 | }); 45 | } else { 46 | return ; 47 | } 48 | } 49 | toggleModalState() { 50 | if(this.state.openModal) { 51 | this.clearFormAndCloseModal(); 52 | } else { 53 | this.setState({ 54 | openModal: true 55 | }) 56 | } 57 | } 58 | handleNewPersonNameChange(e) { 59 | this.setState({ 60 | newPersonName: e.target.value 61 | }); 62 | } 63 | handleNewPersonReasonChange(e) { 64 | this.setState({ 65 | newPersonReason: e.target.value 66 | }); 67 | } 68 | handleNewPersonRewardChange(e) { 69 | this.setState({ 70 | newPersonReward: e.target.value 71 | }); 72 | } 73 | handleNewPersonEyeChange(e) { 74 | this.setState({ 75 | newPersonEyes: e.target.value 76 | }); 77 | } 78 | handleNewPersonNoseChange(e) { 79 | this.setState({ 80 | newPersonNose: e.target.value 81 | }); 82 | } 83 | handleNewPersonMouthChange(e) { 84 | this.setState({ 85 | newPersonMouth: e.target.value 86 | }); 87 | } 88 | handleSkinChange(e) { 89 | this.setState({ 90 | newPersonSkin: e.target.value 91 | }); 92 | } 93 | clearFormAndCloseModal() { 94 | this.setState({ 95 | newPersonName: '', 96 | newPersonReason: '', 97 | newPersonReward: '', 98 | newPersonEyes: 1, 99 | newPersonNose: 1, 100 | newPersonMouth: 1, 101 | newPersonSkin: '#CE96FF', 102 | openModal: false 103 | }); 104 | } 105 | handlePersonCreation() { 106 | const person = { 107 | name: this.state.newPersonName, 108 | image: `https://api.adorable.io/avatars/face/eyes${this.state.newPersonEyes}/nose${this.state.newPersonNose}/mouth${this.state.newPersonMouth}/${this.state.newPersonSkin.slice(1)}`, 109 | reason: this.state.newPersonReason, 110 | reward: this.state.newPersonReward 111 | }; 112 | this.props.addPerson(person); 113 | this.clearFormAndCloseModal(); 114 | } 115 | handleClearToast() { 116 | this.props.clearToast(); 117 | } 118 | render() { 119 | return ( 120 |
121 | {this.props.toast 122 | ? 125 | : null} 126 |
127 |
128 |
129 |

130 | Most Wanted: 131 | 134 |

135 | 136 | {this.renderUsers()} 137 |
138 |
139 | 140 |
141 |
142 |
143 | 162 |
163 | ); 164 | } 165 | } 166 | 167 | //connects root reducer to props 168 | function mapStateToProps(state) { 169 | return { 170 | wantedPeople: state.wantedPeople, 171 | toast: state.toast 172 | } 173 | } 174 | 175 | //connects redux actions to props 176 | function mapDispatchToProps(dispatch) { 177 | return bindActionCreators({ 178 | getWantedList: getWantedList, 179 | addPerson: addPerson, 180 | clearToast: clearToast 181 | }, dispatch); 182 | } 183 | 184 | export default connect(mapStateToProps, mapDispatchToProps)(App); 185 | -------------------------------------------------------------------------------- /src/components/LoadingSpinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingSpinner = () => { 4 | return ( 5 |
6 |
7 |
8 | ); 9 | } 10 | 11 | export default LoadingSpinner; 12 | -------------------------------------------------------------------------------- /src/components/NewUserFace.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NewUserFace = (props) => { 4 | return ( 5 |
7 | 8 | New person avatar 9 |
10 | ); 11 | } 12 | 13 | export default NewUserFace; 14 | -------------------------------------------------------------------------------- /src/components/Note.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Note = (props) => { 4 | return ( 5 |
6 |
7 | {props.edit 8 | ?