diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..826f5ce --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +6.6.0 diff --git a/index.html b/index.html index 5489acf..4eb5886 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,11 @@ Minimal React/Redux Boilerplate +
diff --git a/src/components/filter-link/index.js b/src/components/filter-link/index.js new file mode 100644 index 0000000..e5e93b3 --- /dev/null +++ b/src/components/filter-link/index.js @@ -0,0 +1,14 @@ +import React from 'react'; + +export default ({ + handler = e => console.log( 'no handler method defined' ), + filter, + children +}) => { + return ( + { + e.preventDefault(); + handler() + }}>{ children } + ); +} diff --git a/src/components/filter-link/spec.js b/src/components/filter-link/spec.js new file mode 100644 index 0000000..9f156b6 --- /dev/null +++ b/src/components/filter-link/spec.js @@ -0,0 +1,16 @@ +import React from 'react'; +import test from 'tape'; +import { shallow } from 'enzyme'; + +import FilterLink from './'; + +test( 'FilterLink', t => { + t.plan( 1 ); + + const wrapper = shallow( ); + const expected = true; + const actual = wrapper.is( 'a' ); + + t.ok( actual === expected, 'renders a link' ); +}); + diff --git a/src/components/filter-list/index.js b/src/components/filter-list/index.js new file mode 100644 index 0000000..09fdd87 --- /dev/null +++ b/src/components/filter-list/index.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import showAll from '../../store/actions/show-all'; +import showActive from '../../store/actions/show-active'; +import showCompleted from '../../store/actions/show-completed'; + +import FilterLink from '../filter-link'; + +export class FilterList extends React.Component { + constructor ( props ) { + super( props ) + } + + render () { + const { + onShowAll = e => e, + onShowActive = e => e, + onShowCompleted = e => e, + visibilityFilter = '' + } = this.props; + + return ( + + ); + } +} + +export const mapStateToProps = ({ todos, visibilityFilter }) => ({ + todos, + visibilityFilter, +}); + +export const mapDispatchToProps = dispatch => ({ + onShowAll: () => dispatch( showAll() ), + onShowActive: () => dispatch( showActive() ), + onShowCompleted: () => dispatch( showCompleted() ), +}); + +export default connect( mapStateToProps, mapDispatchToProps )( FilterList ); + diff --git a/src/components/filter-list/spec.js b/src/components/filter-list/spec.js new file mode 100644 index 0000000..e69de29 diff --git a/src/components/todo-item/index.js b/src/components/todo-item/index.js index 9ceeb49..d9010bb 100644 --- a/src/components/todo-item/index.js +++ b/src/components/todo-item/index.js @@ -2,11 +2,13 @@ import React from 'react'; export default ({ remove = e => console.log( 'no remove method defined' ), + toggle = e => console.log( 'no toggle method defined' ), name, + completed }) => { return ( - + {name} X diff --git a/src/components/todo-items/index.js b/src/components/todo-items/index.js index c685870..5769e19 100644 --- a/src/components/todo-items/index.js +++ b/src/components/todo-items/index.js @@ -4,11 +4,29 @@ import TodoItem from '../todo-item'; export default ({ items = [], + filter = 'SHOW_ALL', onRemove = e => console.log( 'no onRemove method defined' ), + onToggle = e => console.log( 'no onToggle method defined' ), }) => { - const nodes = items.map( ( todo, i ) => ( + const getVisibleTodos = (todos, filter) => { + switch ( filter ) { + case 'SHOW_ALL': + return todos; + case 'SHOW_ACTIVE': + return todos.filter( t => !t.completed ); + case 'SHOW_COMPLETED': + return todos.filter( t => t.completed ); + default: + return todos; + } + } + + const nodes = getVisibleTodos(items, filter).map( ( todo, i ) => (
  • - onRemove( todo )} /> + onRemove( todo )} + toggle={() => onToggle( todo )} + />
  • )); diff --git a/src/components/todo-list/index.js b/src/components/todo-list/index.js index 46f80e0..be48255 100644 --- a/src/components/todo-list/index.js +++ b/src/components/todo-list/index.js @@ -3,7 +3,10 @@ import { connect } from 'react-redux'; import addItem from '../../store/actions/add-item'; import removeItem from '../../store/actions/remove-item'; +import toggleItem from '../../store/actions/toggle-item'; + import TodoItems from '../todo-items'; +import FilterList from '../filter-list'; export class TodoList extends React.Component { constructor ( props ) { @@ -22,7 +25,9 @@ export class TodoList extends React.Component { const { onAdd = e => e, onRemove = e => e, + onToggle = e => e, todos = [], + visibilityFilter = 'SHOW_ALL' } = this.props; const { @@ -47,11 +52,15 @@ export class TodoList extends React.Component { ? :

    You have no todos.

    } + + ); } @@ -73,13 +82,15 @@ export class TodoList extends React.Component { } } -export const mapStateToProps = ({ todos }) => ({ +export const mapStateToProps = ({ todos, visibilityFilter }) => ({ todos, + visibilityFilter, }); export const mapDispatchToProps = dispatch => ({ onAdd: v => dispatch( addItem( v ) ), - onRemove: todo => dispatch( removeItem( todo ) ) + onRemove: todo => dispatch( removeItem( todo ) ), + onToggle: todo => dispatch( toggleItem( todo ) ), }); export default connect( mapStateToProps, mapDispatchToProps )( TodoList ); diff --git a/src/store/actions/add-item/index.js b/src/store/actions/add-item/index.js index 8b524d1..1ca290d 100644 --- a/src/store/actions/add-item/index.js +++ b/src/store/actions/add-item/index.js @@ -1,5 +1,6 @@ export default name => ({ type: 'ADD_TODO_ITEM', name, + completed: false }); diff --git a/src/store/actions/show-active/index.js b/src/store/actions/show-active/index.js new file mode 100644 index 0000000..b764770 --- /dev/null +++ b/src/store/actions/show-active/index.js @@ -0,0 +1,4 @@ +export default () => ({ + type: 'SET_VISIBILITY_FILTER', + filter: 'SHOW_ACTIVE' +}); diff --git a/src/store/actions/show-all/index.js b/src/store/actions/show-all/index.js new file mode 100644 index 0000000..72fa750 --- /dev/null +++ b/src/store/actions/show-all/index.js @@ -0,0 +1,4 @@ +export default () => ({ + type: 'SET_VISIBILITY_FILTER', + filter: 'SHOW_ALL' +}); diff --git a/src/store/actions/show-completed/index.js b/src/store/actions/show-completed/index.js new file mode 100644 index 0000000..fcbb3c5 --- /dev/null +++ b/src/store/actions/show-completed/index.js @@ -0,0 +1,4 @@ +export default () => ({ + type: 'SET_VISIBILITY_FILTER', + filter: 'SHOW_COMPLETED' +}); diff --git a/src/store/actions/toggle-item/index.js b/src/store/actions/toggle-item/index.js new file mode 100644 index 0000000..f0d7ef7 --- /dev/null +++ b/src/store/actions/toggle-item/index.js @@ -0,0 +1,5 @@ +export default todo => ({ + type: 'TOGGLE_TODO_ITEM', + todo, +}); + diff --git a/src/store/reducers/index.js b/src/store/reducers/index.js index a876f0c..8806bec 100644 --- a/src/store/reducers/index.js +++ b/src/store/reducers/index.js @@ -1,5 +1,6 @@ import { combineReducers } from 'redux'; import todos from './todos'; +import visibilityFilter from './visibility-filter' -export default combineReducers({ todos }); +export default combineReducers({ todos, visibilityFilter }); diff --git a/src/store/reducers/todos.js b/src/store/reducers/todos.js index e71dda3..ef240a4 100644 --- a/src/store/reducers/todos.js +++ b/src/store/reducers/todos.js @@ -2,12 +2,20 @@ export default ( todos = [], { type, ...payload } ) => { switch ( type ) { case 'ADD_TODO_ITEM': return [ ...todos, payload ]; - break; case 'REMOVE_TODO_ITEM': return todos.filter( t => t !== payload.todo ); - break; + case 'TOGGLE_TODO_ITEM': + return todos.map( t => { + if (t.name !== payload.todo.name) { + return t; + } + return { + ...t, + completed: !t.completed + } + }) + default: + return todos; } - - return todos; }; diff --git a/src/store/reducers/visibility-filter.js b/src/store/reducers/visibility-filter.js new file mode 100644 index 0000000..1cc03c8 --- /dev/null +++ b/src/store/reducers/visibility-filter.js @@ -0,0 +1,8 @@ +export default ( visibilityFilter = 'SHOW_ALL', action ) => { + switch ( action.type ) { + case 'SET_VISIBILITY_FILTER': + return action.filter; + default: + return visibilityFilter; + } +};