diff --git a/package.json b/package.json
index 7bf4a6b..09bff37 100644
--- a/package.json
+++ b/package.json
@@ -13,29 +13,33 @@
"coverage": "NODE_ENV=testing nyc yarn test"
},
"dependencies": {
+ "antd": "^2.6.0",
+ "chai": "^3.5.0",
+ "enzyme": "^2.6.0",
"ramda": "^0.22.1",
"react": "^15.0.2",
"react-addons-test-utils": "^15.4.1",
"react-dom": "^15.0.2",
- "react-redux": "^4.4.5",
+ "react-redux": "^5.0.2",
"react-router": "^3.0.0",
- "redux": "^3.5.2",
- "redux-logger": "^2.6.1",
+ "redux": "^3.6.0",
+ "redux-logger": "^2.7.4",
"redux-thunk": "^2.0.1",
- "styled-components": "^1.2.1"
+ "styled-components": "^1.2.1",
+ "whatwg-fetch": "^2.0.1"
},
"devDependencies": {
- "sinon": "^1.17.7",
- "chai": "^3.5.0",
- "enzyme": "^2.6.0",
"babel-cli": "^6.18.0",
"babel-core": "^6.18.2",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.8",
+ "babel-plugin-import": "^1.1.0",
"babel-preset-latest": "^6.16.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.16.0",
+ "chai": "^3.5.0",
"css-loader": "^0.26.1",
+ "enzyme": "^2.6.0",
"eslint": "^3.11.1",
"eslint-config-airbnb": "^12.0.0",
"eslint-plugin-import": "^2.2.0",
@@ -44,6 +48,7 @@
"extract-text-webpack-plugin": "^2.0.0-beta",
"mocha": "^3.2.0",
"nyc": "^10.0.0",
+ "sinon": "^1.17.7",
"style-loader": "^0.13.1",
"webpack": "^2.1.0-beta.26",
"webpack-bundle-analyzer": "^1.5.0",
@@ -55,6 +60,15 @@
]
},
"babel": {
+ "plugins": [
+ [
+ "import",
+ {
+ "libraryName": "antd",
+ "style": "css"
+ }
+ ]
+ ],
"presets": [
[
"latest",
diff --git a/public/index.html b/public/index.html
index 8b99d88..7be6672 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2,10 +2,18 @@
+
Loading TODO app ...
diff --git a/src/client/actions/index.js b/src/client/actions/index.js
new file mode 100644
index 0000000..b9f0800
--- /dev/null
+++ b/src/client/actions/index.js
@@ -0,0 +1,7 @@
+import * as todo from './todos';
+import * as task from './tasks';
+
+module.exports = {
+ todo,
+ task,
+};
diff --git a/src/client/actions/tasks.js b/src/client/actions/tasks.js
new file mode 100644
index 0000000..519ae3e
--- /dev/null
+++ b/src/client/actions/tasks.js
@@ -0,0 +1,39 @@
+import requestJSON from '../requestJSON';
+import apiURI from '../apiURI';
+
+export const INITIAL_TASKS_LOADED = 'INITIAL_TASKS_LOADED';
+export const TASK_CREATED = 'TASK_CREATED';
+export const TASK_REMOVED = 'TASK_REMOVED';
+export const TASK_UPDATED = 'TASK_UPDATED';
+
+export const taskCreated = payload => ({
+ type: TASK_CREATED,
+ payload,
+});
+
+export const taskRemoved = payload => ({
+ type: TASK_REMOVED,
+ payload,
+});
+
+export const taskUpdated = payload => ({
+ type: TASK_UPDATED,
+ payload,
+});
+
+export const add = payload => (dispatch) => {
+ const body = { task: { ...payload } };
+ requestJSON(`${apiURI}/todo/tasks`, body, 'POST')
+ .then(task => dispatch(taskCreated(task)));
+};
+
+export const remove = payload => (dispatch) => {
+ requestJSON(`${apiURI}/todo/task/${payload}`, {}, 'DELETE')
+ .then(task => dispatch(taskRemoved(task)));
+};
+
+export const update = payload => (dispatch) => {
+ const body = { task: { ...payload } };
+ requestJSON(`${apiURI}/todo/tasks`, body, 'PUT')
+ .then(task => dispatch(taskUpdated(task)));
+};
diff --git a/src/client/actions/todos.js b/src/client/actions/todos.js
new file mode 100644
index 0000000..076a880
--- /dev/null
+++ b/src/client/actions/todos.js
@@ -0,0 +1,39 @@
+import requestJSON from '../requestJSON';
+import apiURI from '../apiURI';
+
+export const INITIAL_TODOS_LOADED = 'INITIAL_TODOS_LOADED';
+export const TODO_CREATED = 'TODO_CREATED';
+export const TODO_REMOVED = 'TODO_REMOVED';
+export const TODO_UPDATED = 'TODO_UPDATED';
+
+export const todoCreated = payload => ({
+ type: TODO_CREATED,
+ payload,
+});
+
+export const todoUpdated = payload => ({
+ type: TODO_UPDATED,
+ payload,
+});
+
+export const todoRemoved = payload => ({
+ type: TODO_REMOVED,
+ payload,
+});
+
+export const create = payload => (dispatch) => {
+ const body = { todo: payload };
+ requestJSON(`${apiURI}/todo/lists`, body, 'POST')
+ .then(todo => dispatch(todoCreated(todo)));
+};
+
+export const update = payload => (dispatch) => {
+ const body = { todo: payload };
+ requestJSON(`${apiURI}/todo/lists`, body, 'PUT')
+ .then(todo => dispatch(todoUpdated(todo)));
+};
+
+export const remove = payload => (dispatch) => {
+ requestJSON(`${apiURI}/todo/list/${payload}`, {}, 'DELETE')
+ .then(todo => dispatch(todoRemoved(todo)));
+};
diff --git a/src/client/apiURI.js b/src/client/apiURI.js
new file mode 100644
index 0000000..d361101
--- /dev/null
+++ b/src/client/apiURI.js
@@ -0,0 +1 @@
+export default 'http://rp3.redpelicans.com:4007/api';
diff --git a/src/client/colors.json b/src/client/colors.json
new file mode 100644
index 0000000..6731c71
--- /dev/null
+++ b/src/client/colors.json
@@ -0,0 +1,21 @@
+{
+ "red": "#f44336",
+ "pink": "#e91e63",
+ "purple": "#9c27b0",
+ "deepPurple": "#673ab7",
+ "indigo": "#3f51b5",
+ "blue": "#2196f3",
+ "lightBlue": "#03a9f4",
+ "cyan": "#00bcd4",
+ "teal": "#009688",
+ "green": "#4caf50",
+ "lightGreen": "#8bc34a",
+ "lime": "#cddc39",
+ "yellow": "#ffeb3b",
+ "amber": "#ffc107",
+ "orange": "#ff9800",
+ "deepOrange": "#ff5722",
+ "brown": "#795548",
+ "grey": "#9e9e9e",
+ "blueGrey": "#607d8b"
+}
diff --git a/src/client/components/AddTask/index.js b/src/client/components/AddTask/index.js
new file mode 100644
index 0000000..88b15ef
--- /dev/null
+++ b/src/client/components/AddTask/index.js
@@ -0,0 +1,67 @@
+import React, { PropTypes } from 'react';
+import Modal from 'antd/lib/modal';
+
+import Button from '../Button';
+import FullHeightContainer from '../FullHeightContainer';
+import ModalInput from '../ModalInput';
+
+export default class AddTask extends React.Component {
+ state = {
+ visible: false,
+ value: '',
+ }
+
+ handleKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ this.saveTask();
+ }
+ }
+
+ close = () => this.setState({ visible: false })
+
+ showModal = () => this.setState({ visible: true })
+
+ handleChange = e => this.setState({ value: e.target.value });
+
+ saveTask = () => {
+ const { value } = this.state;
+ const { actions, listId } = this.props;
+
+ actions.task.add({ description: value, isCompleted: false, listId });
+ this.setState({ value: '', visible: false });
+ }
+
+ render() {
+ const { visible, value } = this.state;
+ return (
+
+
+ Cancel,
+ ,
+ ]}
+ >
+
+
+
+ );
+ }
+}
+
+AddTask.propTypes = {
+ actions: PropTypes.object.isRequired,
+ listId: PropTypes.number.isRequired,
+};
diff --git a/src/client/components/AddTodo/index.js b/src/client/components/AddTodo/index.js
new file mode 100644
index 0000000..3b32f62
--- /dev/null
+++ b/src/client/components/AddTodo/index.js
@@ -0,0 +1,72 @@
+import React, { PropTypes } from 'react';
+import styled from 'styled-components';
+import colors from '../../colors.json';
+
+import TodoInput from '../TodoInput';
+
+const FormContainer = styled.form`
+ background-color: white;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
+ display: flex;
+ margin-top: 2em;
+ width: 50vw;
+ height: 50px;
+`;
+
+const TodoSubmit = styled.input`
+ border: none;
+ background-color: ${colors.blueGrey};
+ width: 50%;
+ height: 100%;
+ color: white;
+ font-size: 16px;
+ cursor: pointer;
+ transition: all .2s;
+
+ &:hover {
+ background-color: white;
+ color: ${colors.blueGrey};
+ }
+`;
+
+export default class AddTodo extends React.Component {
+ state = {
+ value: '',
+ }
+
+ handleChange = e => this.setState({ value: e.target.value });
+
+ addTodo = (e) => {
+ e.preventDefault();
+
+ const { actions } = this.props;
+ const { value } = this.state;
+
+ if (!value) return false;
+
+ actions.todo.create({ label: value });
+ this.setState({ value: '' });
+ return true;
+ }
+
+ render() {
+ const { value } = this.state;
+ return (
+
+
+
+
+ );
+ }
+}
+
+AddTodo.propTypes = {
+ actions: PropTypes.object.isRequired,
+};
diff --git a/src/client/components/App/__test__/index.js b/src/client/components/App/__test__/index.js
deleted file mode 100644
index 067d243..0000000
--- a/src/client/components/App/__test__/index.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import chai from 'chai';
-import { shallow } from 'enzyme';
-import App, { Title } from '..';
-
-const { describe, it } = global;
-const { expect } = chai;
-
-describe('[UT]
', () => {
- it('should render a
', () => {
- expect(shallow(
).find(Title)).to.have.length(1);
- });
-});
diff --git a/src/client/components/App/index.js b/src/client/components/App/index.js
deleted file mode 100644
index ff82195..0000000
--- a/src/client/components/App/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-
-export const Title = styled.h1`
- font-size: 1.5em;
- text-align: center;
- color: palevioletred;
-`;
-
-const Wrapper = styled.section`
- padding: 4em;
- background: papayawhip;
-`;
-
-const App = () => (
-
- Hello World, this is my first react app!
-
-);
-
-export default App;
diff --git a/src/client/components/Button/index.js b/src/client/components/Button/index.js
index d17e537..ca0695f 100644
--- a/src/client/components/Button/index.js
+++ b/src/client/components/Button/index.js
@@ -1,9 +1,18 @@
-import React, { PropTypes } from 'react';
+import styled from 'styled-components';
+import colors from '../../colors.json';
-const Button = ({ onClick }) =>
;
-
-Button.propTypes = {
- onClick: PropTypes.func.isRequired,
-};
+const Button = styled.button`
+ border: none;
+ width: 4em;
+ height: 100%;
+ cursor: pointer;
+ transition: all .2s;
+ font-size: 20px;
+ background-color: white;
+ &:hover {
+ background-color: ${colors.blueGrey};
+ color: white;
+ }
+`;
export default Button;
diff --git a/src/client/components/Container/index.js b/src/client/components/Container/index.js
new file mode 100644
index 0000000..01f7a31
--- /dev/null
+++ b/src/client/components/Container/index.js
@@ -0,0 +1,7 @@
+import styled from 'styled-components';
+
+export default styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
diff --git a/src/client/components/FullHeightContainer/index.js b/src/client/components/FullHeightContainer/index.js
new file mode 100644
index 0000000..20f24a3
--- /dev/null
+++ b/src/client/components/FullHeightContainer/index.js
@@ -0,0 +1,7 @@
+import styled from 'styled-components';
+
+const FullHeightContainer = styled.div`
+ height: 100%;
+`;
+
+export default FullHeightContainer;
diff --git a/src/client/components/Header/index.js b/src/client/components/Header/index.js
new file mode 100644
index 0000000..0fecf08
--- /dev/null
+++ b/src/client/components/Header/index.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import styled from 'styled-components';
+
+const CenteredContainer = styled.header`
+ background-color: white;
+ width: 100vw;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
+ font-family: Roboto, arial, verdana;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+`;
+
+const Title = styled.h1`
+ font-size: 3em;
+ padding: 0;
+ text-align: center;
+ color: black;
+`;
+
+const Header = () => (
+
+ TODO APP
+
+);
+
+export default Header;
diff --git a/src/client/components/ModalInput/index.js b/src/client/components/ModalInput/index.js
new file mode 100644
index 0000000..286481c
--- /dev/null
+++ b/src/client/components/ModalInput/index.js
@@ -0,0 +1,9 @@
+import styled from 'styled-components';
+
+const ModalInput = styled.input`
+ width: 100%;
+ padding: 1em;
+ font-size: 15px;
+`;
+
+export default ModalInput;
diff --git a/src/client/components/RemoveTodo/index.js b/src/client/components/RemoveTodo/index.js
new file mode 100644
index 0000000..617ebf6
--- /dev/null
+++ b/src/client/components/RemoveTodo/index.js
@@ -0,0 +1,46 @@
+import React, { PropTypes } from 'react';
+
+import Modal from 'antd/lib/modal';
+import FullHeightContainer from '../FullHeightContainer';
+import Button from '../Button';
+
+export default class RemoveTodo extends React.Component {
+ state = {
+ visible: false,
+ }
+
+ close = () => this.setState({ visible: false })
+
+ showModal = () => this.setState({ visible: true })
+
+ removeTodo = () => {
+ const { actions, listId } = this.props;
+
+ actions.todo.remove(listId);
+ }
+
+ render() {
+ const { visible } = this.state;
+ return (
+
+
+ NO,
+ ,
+ ]}
+ >
+ ARE YOU SURE ?
+
+
+ );
+ }
+}
+
+RemoveTodo.propTypes = {
+ actions: PropTypes.object.isRequired,
+ listId: PropTypes.number.isRequired,
+};
diff --git a/src/client/components/TodoContainer/index.js b/src/client/components/TodoContainer/index.js
new file mode 100644
index 0000000..621f280
--- /dev/null
+++ b/src/client/components/TodoContainer/index.js
@@ -0,0 +1,45 @@
+import React, { PropTypes } from 'react';
+import styled from 'styled-components';
+
+import AddTodo from '../AddTodo';
+import MainContainer from '../Container';
+import TodoEl from '../TodoEl';
+
+const TodoList = styled.ul`
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ margin: 0;
+ padding: 0;
+`;
+
+const linkTasks = (id, tasks) => tasks.filter(task => task.listId === id);
+
+const drawTodos = (todos, tasks, actions) => todos.map(todo =>
+
+ {todo.label}
+ ,
+);
+
+const TodoContainer = ({ todos, tasks, actions }) =>
+
+
+
+ {drawTodos(todos, tasks, actions)}
+
+
+;
+
+TodoContainer.propTypes = {
+ todos: PropTypes.array.isRequired,
+ tasks: PropTypes.array.isRequired,
+ actions: PropTypes.object.isRequired,
+};
+
+export default TodoContainer;
diff --git a/src/client/components/TodoEl/index.js b/src/client/components/TodoEl/index.js
new file mode 100644
index 0000000..fcafa63
--- /dev/null
+++ b/src/client/components/TodoEl/index.js
@@ -0,0 +1,101 @@
+import React, { PropTypes } from 'react';
+import styled from 'styled-components';
+
+import AddTask from '../AddTask';
+import RemoveTodo from '../RemoveTodo';
+import UpdateTask from '../UpdateTask';
+import UpdateTodo from '../UpdateTodo';
+import Button from '../Button';
+
+const TodoBlock = styled.li`
+ list-style: none;
+ font-family: Roboto, arial, verdana;
+ background-color: white;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
+ display: flex;
+ flex-direction: column;
+ margin: 2em;
+ border-radius: 2px;
+`;
+
+const TaskContainer = styled.li`
+ list-style: none;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all .2s;
+`;
+
+const TasksList = styled.ul`
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+`;
+
+const TodoTitle = styled.span`
+ margin: 1em 1em;
+`;
+
+const TaskTitle = styled.span`
+ margin: 0 .5em;
+ cursor: pointer;
+ text-decoration: ${props => (props.checked ? 'line-through' : 'none')};
+ color: ${props => (props.checked ? 'grey' : 'black')}
+`;
+
+const TopTodo = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ height: 50px;
+ background-color: rgba(0,0,0,.1);
+ & button {
+ background-color: rgba(0,0,0,0);
+ }
+`;
+
+const TaskControls = styled.div`
+ display: flex;
+`;
+
+const drawTasks = (tasks, actions) => tasks.map(task =>
+
+ actions.task.update({
+ ...task,
+ isCompleted: !task.isCompleted,
+ })}
+ >
+ {task.description}
+
+
+
+
+
+ ,
+);
+
+const TodoEl = ({ children, tasks, actions, listId }) =>
+
+
+ {children}
+
+
+
+
+ {drawTasks(tasks, actions)}
+
+;
+
+TodoEl.propTypes = {
+ children: PropTypes.string.isRequired,
+ tasks: PropTypes.array.isRequired,
+ actions: PropTypes.object.isRequired,
+ listId: PropTypes.number.isRequired,
+};
+
+export default TodoEl;
diff --git a/src/client/components/TodoInput/index.js b/src/client/components/TodoInput/index.js
new file mode 100644
index 0000000..014c7f2
--- /dev/null
+++ b/src/client/components/TodoInput/index.js
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+
+const TodoInput = styled.input`
+ width: 80%;
+ height: 2em;
+ border: none;
+ padding-top: 1em;
+ padding-left: 1em;
+ font-size: 16px;
+`;
+
+export default TodoInput;
diff --git a/src/client/components/UpdateTask/index.js b/src/client/components/UpdateTask/index.js
new file mode 100644
index 0000000..be226e3
--- /dev/null
+++ b/src/client/components/UpdateTask/index.js
@@ -0,0 +1,73 @@
+import React, { PropTypes } from 'react';
+
+import Modal from 'antd/lib/modal';
+import Button from '../Button';
+import ModalInput from '../ModalInput';
+
+export default class UpdateTask extends React.Component {
+ state = {
+ visible: false,
+ value: this.props.task.description,
+ }
+
+ componentWillReceiveProps = (newProps) => {
+ this.setState({ value: newProps.task.description });
+ }
+
+ updateTask = () => {
+ const { value } = this.state;
+ const { actions, task } = this.props;
+ const { id, isCompleted, listId } = task;
+
+ if (!value || value.length > 1000) return false;
+ actions.task.update({ listId, id, description: value, isCompleted });
+ this.setState({ visible: false });
+ return (true);
+ }
+
+ close = () => this.setState({ visible: false })
+
+ showModal = () => this.setState({ visible: true });
+
+ handleChange = e => this.setState({ value: e.target.value })
+
+ handleKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ this.updateTask();
+ }
+ }
+
+ render() {
+ const { value, visible } = this.state;
+ return (
+
+
+ Cancel,
+ ,
+ ]}
+ >
+
+
+
+ );
+ }
+}
+
+UpdateTask.propTypes = {
+ actions: PropTypes.object.isRequired,
+ task: PropTypes.object.isRequired,
+};
diff --git a/src/client/components/UpdateTodo/index.js b/src/client/components/UpdateTodo/index.js
new file mode 100644
index 0000000..1b2ecf3
--- /dev/null
+++ b/src/client/components/UpdateTodo/index.js
@@ -0,0 +1,74 @@
+import React, { PropTypes } from 'react';
+
+import Modal from 'antd/lib/modal';
+import Button from '../Button';
+import FullHeightContainer from '../FullHeightContainer';
+import ModalInput from '../ModalInput';
+
+export default class UpdateTodo extends React.Component {
+ state = {
+ visible: false,
+ value: this.props.label,
+ }
+
+ componentWillReceiveProps = (newProps) => {
+ this.setState({ value: newProps.label });
+ }
+
+ updateTodo = () => {
+ const { value } = this.state;
+ const { actions, listId } = this.props;
+
+ if (!value || value.length > 1000) return false;
+ actions.todo.update({ id: listId, label: value });
+ this.setState({ visible: false });
+ return (true);
+ }
+
+ close = () => this.setState({ visible: false })
+
+ showModal = () => this.setState({ visible: true });
+
+ handleChange = e => this.setState({ value: e.target.value })
+
+ handleKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ this.updateTodo();
+ }
+ }
+
+ render() {
+ const { value, visible } = this.state;
+ return (
+
+
+ Cancel,
+ ,
+ ]}
+ >
+
+
+
+ );
+ }
+}
+
+UpdateTodo.propTypes = {
+ label: PropTypes.string.isRequired,
+ actions: PropTypes.object.isRequired,
+ listId: PropTypes.number.isRequired,
+};
diff --git a/src/client/containers/App/__test__/index.js b/src/client/containers/App/__test__/index.js
new file mode 100644
index 0000000..9eaf40a
--- /dev/null
+++ b/src/client/containers/App/__test__/index.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import chai from 'chai';
+import { shallow } from 'enzyme';
+
+import App from '..';
+import Header from '../../Header';
+import TodoContainer from '../../TodoContainer';
+import actions from '../../../actions';
+
+const state = [
+ {
+ title: 'my first todoList',
+ id: 0,
+ checked: true,
+ tasks: [
+ { title: 'my first task', checked: false, id: 0 },
+ { title: 'my second task', checked: true, id: 1 },
+ { title: 'my third task', checked: false, id: 2 },
+ { title: 'my fourth task', checked: false, id: 3 },
+ { title: 'my fifth task', checked: true, id: 4 },
+ { title: 'my sixth task', checked: false, id: 5 },
+ ],
+ },
+ {
+ title: 'my second todoList',
+ checked: false,
+ id: 1,
+ tasks: [
+ { title: 'my first task', checked: false, id: 6 },
+ { title: 'my second task', checked: false, id: 7 },
+ { title: 'my third task', checked: true, id: 8 },
+ ],
+ },
+];
+
+const store = {
+ state,
+ listeners: [],
+ listen(cb) { this.listeners.push(cb); },
+ callListeners() { this.listeners.forEach(cb => cb()); },
+ dispatch(action) {
+ this.state = action(this.state);
+ this.callListeners();
+ },
+};
+
+const { describe, it } = global;
+const { expect } = chai;
+
+describe('
', () => {
+ const app = shallow(
);
+ it('should render a
', () => {
+ expect(app.find(Header)).to.have.length(1);
+ });
+ it('should render a
', () => {
+ expect(app.find(TodoContainer)).to.have.length(1);
+ });
+ it('should have store in props', () => {
+ expect(app.props().store).to.equal(store);
+ });
+ it('should have actions in props', () => {
+ expect(app.props().actions).to.equal(actions);
+ });
+});
diff --git a/src/client/containers/App/index.js b/src/client/containers/App/index.js
new file mode 100644
index 0000000..a1d9047
--- /dev/null
+++ b/src/client/containers/App/index.js
@@ -0,0 +1,30 @@
+import React, { PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import allTheActions from '../../actions';
+import Header from '../../components/Header';
+import TodoContainer from '../../components/TodoContainer';
+import MainContainer from '../../components/Container';
+
+const App = ({ todos, tasks, actions }) =>
+
+
+
+
+;
+
+const mapStateToPros = ({ todos, tasks }) => ({ todos, tasks });
+const mapDispatchToProps = dispatch => ({
+ actions: {
+ todo: bindActionCreators(allTheActions.todo, dispatch),
+ task: bindActionCreators(allTheActions.task, dispatch),
+ },
+});
+
+App.propTypes = {
+ todos: PropTypes.array.isRequired,
+ tasks: PropTypes.array.isRequired,
+ actions: PropTypes.object.isRequired,
+};
+
+export default connect(mapStateToPros, mapDispatchToProps)(App);
diff --git a/src/client/index.js b/src/client/index.js
index da1841c..f39445f 100644
--- a/src/client/index.js
+++ b/src/client/index.js
@@ -1,7 +1,16 @@
import React from 'react';
import { render } from 'react-dom';
-import App from './components/App';
+import 'antd/dist/antd.css';
+import { Provider } from 'react-redux';
+import App from './containers/App';
+import store from './store';
-console.log('mounting react app ...'); // eslint-disable-line no-console
-render(
, document.getElementById('__TODO__'));
+const Root = (
+
+
+
+);
+console.log('mounting react app ...'); // eslint-disable-line no-console
+
+render(Root, document.getElementById('__TODO__'));
diff --git a/src/client/initialState.js b/src/client/initialState.js
new file mode 100644
index 0000000..76ce512
--- /dev/null
+++ b/src/client/initialState.js
@@ -0,0 +1,26 @@
+import apiURI from './apiURI';
+
+export const state = {
+ todos: [],
+ tasks: [],
+};
+
+const fetchInitial = (uri, cb) => {
+ fetch(uri)
+ .then((data) => {
+ if (data.status === 200) {
+ if (data.json) return data.json();
+ }
+ return null;
+ })
+ .then(json => cb(json))
+ .catch(console.error); // eslint-disable-line no-console
+};
+
+export const getInitialTodos = (cb) => {
+ fetchInitial(`${apiURI}/todo/lists`, cb);
+};
+
+export const getInitialTasks = (cb) => {
+ fetchInitial(`${apiURI}/todo/tasks`, cb);
+};
diff --git a/src/client/reducers/index.js b/src/client/reducers/index.js
new file mode 100644
index 0000000..7802937
--- /dev/null
+++ b/src/client/reducers/index.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux';
+import todos from './todos';
+import tasks from './tasks';
+
+export default combineReducers({
+ todos,
+ tasks,
+});
diff --git a/src/client/reducers/tasks.js b/src/client/reducers/tasks.js
new file mode 100644
index 0000000..db76dee
--- /dev/null
+++ b/src/client/reducers/tasks.js
@@ -0,0 +1,27 @@
+import {
+ TASK_CREATED,
+ TASK_UPDATED,
+ TASK_REMOVED,
+ INITIAL_TASKS_LOADED,
+} from '../actions/tasks';
+
+const updateTask = (state, payload) => state.map((task) => {
+ if (task.id === payload.id) {
+ return { ...payload };
+ }
+ return task;
+});
+
+export default (state = [], action) => {
+ switch (action.type) {
+ case INITIAL_TASKS_LOADED:
+ return action.payload;
+ case TASK_CREATED:
+ return [...state, action.payload];
+ case TASK_REMOVED:
+ return state.filter(task => task.id !== action.payload.id);
+ case TASK_UPDATED:
+ return updateTask(state, action.payload);
+ default: return state;
+ }
+};
diff --git a/src/client/reducers/todos.js b/src/client/reducers/todos.js
new file mode 100644
index 0000000..94455a7
--- /dev/null
+++ b/src/client/reducers/todos.js
@@ -0,0 +1,27 @@
+import {
+ TODO_CREATED,
+ TODO_REMOVED,
+ TODO_UPDATED,
+ INITIAL_TODOS_LOADED,
+} from '../actions/todos';
+
+const updateTodo = (state, payload) => state.map((todo) => {
+ if (todo.id === payload.id) {
+ return { ...payload };
+ }
+ return todo;
+});
+
+export default (state = [], action) => {
+ switch (action.type) {
+ case INITIAL_TODOS_LOADED:
+ return action.payload;
+ case TODO_CREATED:
+ return [...state, action.payload];
+ case TODO_REMOVED:
+ return state.filter(todo => todo.id !== action.payload.id);
+ case TODO_UPDATED:
+ return updateTodo(state, action.payload);
+ default: return state;
+ }
+};
diff --git a/src/client/requestJSON.js b/src/client/requestJSON.js
new file mode 100644
index 0000000..8295c5a
--- /dev/null
+++ b/src/client/requestJSON.js
@@ -0,0 +1,18 @@
+const getJSON = response => (response.json ? response.json() : null);
+
+const checkResponse = (response) => {
+ if (response.status && response.status === 200) {
+ return getJSON(response);
+ }
+ console.error(response); // eslint-disable-line no-console
+ return null;
+};
+
+const requestJSON = (uri, body, method) => {
+ const headers = { 'Content-Type': 'application/json' };
+ const options = { headers, body: JSON.stringify(body), method };
+ return fetch(uri, options)
+ .then(checkResponse);
+};
+
+export default requestJSON;
diff --git a/src/client/store.js b/src/client/store.js
new file mode 100644
index 0000000..fa5b250
--- /dev/null
+++ b/src/client/store.js
@@ -0,0 +1,41 @@
+import { createStore, applyMiddleware } from 'redux';
+import createLogger from 'redux-logger';
+import thunkMiddleware from 'redux-thunk';
+import reducers from './reducers';
+import { initialState, getInitialTodos, getInitialTasks } from './initialState';
+import { INITIAL_TODOS_LOADED } from './actions/todos';
+import { INITIAL_TASKS_LOADED } from './actions/tasks';
+
+const logger = createLogger();
+
+const generateStore = (mode) => {
+ if (mode === 'development') {
+ return createStore(
+ reducers,
+ initialState,
+ applyMiddleware(
+ logger,
+ thunkMiddleware,
+ ),
+ );
+ }
+ return createStore(
+ reducers,
+ initialState,
+ applyMiddleware(
+ thunkMiddleware,
+ ),
+ );
+};
+
+const store = generateStore(process.env.NODE_ENV);
+
+getInitialTodos((data) => {
+ store.dispatch({ type: INITIAL_TODOS_LOADED, payload: data });
+});
+
+getInitialTasks((data) => {
+ store.dispatch({ type: INITIAL_TASKS_LOADED, payload: data });
+});
+
+export default store;
diff --git a/webpack.config.js b/webpack.config.js
index 10104f4..ac8b351 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -17,7 +17,7 @@ const webpackConfig = {
output: {
filename: 'bundle.js',
path: path.join(__dirname, '/build'),
- publicPath: `/assets/`,
+ publicPath: '/assets/',
},
entry: {
app: './src/client/index.js',
@@ -26,7 +26,7 @@ const webpackConfig = {
rules: [
{
test: /\.(js|jsx)$/,
- loader: 'babel-loader',
+ loaders: [ 'babel-loader?cacheDirectory' ],
exclude: /node_modules/,
},
{
@@ -44,8 +44,8 @@ const webpackConfig = {
}),
ifDev(new webpack.HotModuleReplacementPlugin()),
]),
- performance: {
- hints: false
+ performance: {
+ hints: false,
},
stats: {
assets: true,
@@ -60,4 +60,3 @@ const webpackConfig = {
};
module.exports = webpackConfig;
-