From 8f6ece203613b303e6882dc5724b3d0c36bb4b0c Mon Sep 17 00:00:00 2001 From: eric Date: Fri, 6 Jan 2017 09:29:43 +0100 Subject: [PATCH 1/7] Add cacheDirectory for babel loader --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 10104f4..6ce790b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,7 @@ const webpackConfig = { rules: [ { test: /\.(js|jsx)$/, - loader: 'babel-loader', + loaders: [ 'babel-loader?cacheDirectory' ], exclude: /node_modules/, }, { From f0404d844a607e1093db17b7ee095a0b6f85eb22 Mon Sep 17 00:00:00 2001 From: Raphael LE MINOR Date: Mon, 2 Jan 2017 18:28:18 +0100 Subject: [PATCH 2/7] todo 1 dim --- public/index.html | 12 +++- src/client/colors.json | 21 +++++++ src/client/components/CreateTodoForm/index.js | 58 +++++++++++++++++++ src/client/components/Header/index.js | 41 +++++++++++++ src/client/components/Todo/index.js | 56 ++++++++++++++++++ src/client/components/TodoEl/index.js | 50 ++++++++++++++++ src/client/index.js | 1 - webpack.config.js | 7 +-- 8 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 src/client/colors.json create mode 100644 src/client/components/CreateTodoForm/index.js create mode 100644 src/client/components/Header/index.js create mode 100644 src/client/components/Todo/index.js create mode 100644 src/client/components/TodoEl/index.js diff --git a/public/index.html b/public/index.html index 8b99d88..a69e7bc 100644 --- a/public/index.html +++ b/public/index.html @@ -2,10 +2,18 @@ RedTodo - + + -
+
Loading TODO app ...
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/CreateTodoForm/index.js b/src/client/components/CreateTodoForm/index.js new file mode 100644 index 0000000..1d22786 --- /dev/null +++ b/src/client/components/CreateTodoForm/index.js @@ -0,0 +1,58 @@ +import React, { PropTypes } from 'react'; +import styled from 'styled-components'; +import colors from '../../colors.json'; + +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 TodoInput = styled.input` + width: 80%; + height: 2em; + border: none; + padding: 9px; + font-size: 16px; +`; + +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}; + } +`; + +const CreateTodoForm = ({ onSubmit, onChange, inputValue }) => + + + + +; + +CreateTodoForm.propTypes = { + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + inputValue: PropTypes.string.isRequired, +}; + +export default CreateTodoForm; diff --git a/src/client/components/Header/index.js b/src/client/components/Header/index.js new file mode 100644 index 0000000..0856da3 --- /dev/null +++ b/src/client/components/Header/index.js @@ -0,0 +1,41 @@ +import React from 'react'; +import styled from 'styled-components'; +import colors from '../../colors.json'; + +const CenteredContainer = styled.header` + background-color: white; + width: 90vw; + 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; + text-align: center; + color: black; +`; + +const SubTitle = styled.h3` + color: black; +`; + +const Highlighter = styled.span` + margin: 0 10px; + color: ${colors.blueGrey}; +`; + +const Header = () => ( + + TODO APP + + MADE USING + Styled Component + + +); + +export default Header; diff --git a/src/client/components/Todo/index.js b/src/client/components/Todo/index.js new file mode 100644 index 0000000..cd56593 --- /dev/null +++ b/src/client/components/Todo/index.js @@ -0,0 +1,56 @@ +import React from 'react'; +import styled from 'styled-components'; + +import CreateTodoForm from '../CreateTodoForm'; +import TodoEl from '../TodoEl'; + +const TodoList = styled.ul` + display: flex; + flex-direction: column; + align-items: center; + padding: 0; + margin: 0; +`; + +export default class Todo extends React.Component { + state = { + todos: [], + inputValue: '', + }; + + saveTodo = (e) => { + e.preventDefault(); + const { value } = e.target.todo; + if (!value || value === '' || value.length > 1000) return false; + const { todos } = this.state; + const newTodos = [e.target.todo.value, ...todos]; + this.setState({ todos: newTodos, inputValue: '' }); + return true; + } + + updateValue = e => this.setState({ inputValue: e.target.value }); + + remove = index => this.setState({ + todos: this.state.todos.filter((el, i) => index !== i), + }); + + drawTodos = todos => todos.map((el, key) => + this.remove(key)}>{el} + ); + + render() { + const { inputValue, todos } = this.state; + return ( +
+ + + {this.drawTodos(todos)} + +
+ ); + } +} diff --git a/src/client/components/TodoEl/index.js b/src/client/components/TodoEl/index.js new file mode 100644 index 0000000..8e4017d --- /dev/null +++ b/src/client/components/TodoEl/index.js @@ -0,0 +1,50 @@ +import React, { PropTypes } from 'react'; +import styled from 'styled-components'; +import colors from '../../colors.json'; + +const TodoLi = styled.li` + list-style: none; + width: 90%; + height: 3em; + font-family: Roboto, arial, verdana; + background-color: white; + margin-top: 10px; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + display: flex; + justify-content: space-between; + align-items: center; +`; + +const RemoveButton = styled.button` + border: none; + background-color: ${colors.blueGrey}; + width: 4em; + height: 100%; + cursor: pointer; + transition: all .2s; + color: white; + font-size: 20px; + + &:hover { + background-color: white; + color: ${colors.blueGrey}; + } +`; + +const TodoText = styled.span` + margin-left: 1em; +`; + +const todoEl = ({ children, onClick }) => + + {children} + + +; + +todoEl.propTypes = { + children: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, +}; + +export default todoEl; diff --git a/src/client/index.js b/src/client/index.js index da1841c..84ecaca 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -4,4 +4,3 @@ import App from './components/App'; console.log('mounting react app ...'); // eslint-disable-line no-console render(, document.getElementById('__TODO__')); - diff --git a/webpack.config.js b/webpack.config.js index 6ce790b..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', @@ -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; - From 48d4ab333743d4cbb160f28b2b88c61acd52f648 Mon Sep 17 00:00:00 2001 From: Raphael LE MINOR Date: Tue, 3 Jan 2017 21:52:01 +0100 Subject: [PATCH 3/7] remove seems ok --- src/client/actions/index.js | 5 ++ src/client/actions/todos.js | 70 ++++++++++++++++++ src/client/components/App/index.js | 33 ++++++--- src/client/components/CreateTodoForm/index.js | 12 +-- src/client/components/Todo/index.js | 63 +++++----------- src/client/components/TodoContainer/index.js | 41 ++++++++++ src/client/components/TodoEl/index.js | 74 +++++++++++++------ src/client/index.js | 43 ++++++++++- 8 files changed, 254 insertions(+), 87 deletions(-) create mode 100644 src/client/actions/index.js create mode 100644 src/client/actions/todos.js create mode 100644 src/client/components/TodoContainer/index.js diff --git a/src/client/actions/index.js b/src/client/actions/index.js new file mode 100644 index 0000000..ce942e1 --- /dev/null +++ b/src/client/actions/index.js @@ -0,0 +1,5 @@ +import * as todo from './todos'; + +module.exports = { + todo, +}; diff --git a/src/client/actions/todos.js b/src/client/actions/todos.js new file mode 100644 index 0000000..06df963 --- /dev/null +++ b/src/client/actions/todos.js @@ -0,0 +1,70 @@ +/* +* All the actions, +* state = [todo1, todo2, todo3, ...] +* todo = { +* id: Number, +* title: String, +* checked: boolean, +* tasks: Array +* } +* tasks = { +* id: Number, +* title: String, +* Checked: boolean, +* } +*/ + +let taID = 9; +let toID = 2; + +export const createTodo = title => (state) => { + const newTodo = { + checked: false, + id: toID += 1, + tasks: [], + title, + }; + return [...state, newTodo]; +}; + +export const removeTodo = id => state => state.filter(el => el.id !== id); + +export const addTask = payload => state => state.map((todo) => { + if (todo.id === payload.todoID) { + const newTask = { + title: payload.title, + checked: false, + id: taID += 1, + }; + return { ...todo, tasks: [...todo.tasks, newTask] }; + } + return todo; +}); + +/* +* seems ok +*/ +export const removeTask = payload => state => state.map((todo) => { + if (todo.id === payload.todoID) { + const newTasks = todo.tasks.filter(task => task.id !== payload.taskID); + return { ...todo, tasks: newTasks }; + } + return todo; +}); + +export const updateTask = payload => state => state.map((todo) => { + if (todo.id === payload.todoID) { + const newTasks = todo.tasks.map((task) => { + if (task.id === payload.taskID) { + return { + title: payload.title, + checked: payload.checked, + id: task.id, + }; + } + return task; + }); + return { ...todo, tasks: newTasks }; + } + return todo; +}); diff --git a/src/client/components/App/index.js b/src/client/components/App/index.js index ff82195..e382488 100644 --- a/src/client/components/App/index.js +++ b/src/client/components/App/index.js @@ -1,5 +1,7 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import styled from 'styled-components'; +import Header from '../Header'; +import TodoContainer from '../TodoContainer'; export const Title = styled.h1` font-size: 1.5em; @@ -7,15 +9,24 @@ export const Title = styled.h1` color: palevioletred; `; -const Wrapper = styled.section` - padding: 4em; - background: papayawhip; -`; +export default class App extends React.Component { + componentDidMount() { + const { store } = this.props; + store.listen(() => this.forceUpdate()); + } -const App = () => ( - - Hello World, this is my first react app! - -); + render() { + const { store, actions } = this.props; + return ( + +
+ + + ); + } +} -export default App; +App.propTypes = { + store: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, +}; diff --git a/src/client/components/CreateTodoForm/index.js b/src/client/components/CreateTodoForm/index.js index 1d22786..e3639fb 100644 --- a/src/client/components/CreateTodoForm/index.js +++ b/src/client/components/CreateTodoForm/index.js @@ -35,24 +35,16 @@ const TodoSubmit = styled.input` } `; -const CreateTodoForm = ({ onSubmit, onChange, inputValue }) => - +const CreateTodoForm = () => + ; -CreateTodoForm.propTypes = { - onSubmit: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - inputValue: PropTypes.string.isRequired, -}; - export default CreateTodoForm; diff --git a/src/client/components/Todo/index.js b/src/client/components/Todo/index.js index cd56593..9fb4c4c 100644 --- a/src/client/components/Todo/index.js +++ b/src/client/components/Todo/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; import styled from 'styled-components'; import CreateTodoForm from '../CreateTodoForm'; @@ -12,45 +12,22 @@ const TodoList = styled.ul` margin: 0; `; -export default class Todo extends React.Component { - state = { - todos: [], - inputValue: '', - }; - - saveTodo = (e) => { - e.preventDefault(); - const { value } = e.target.todo; - if (!value || value === '' || value.length > 1000) return false; - const { todos } = this.state; - const newTodos = [e.target.todo.value, ...todos]; - this.setState({ todos: newTodos, inputValue: '' }); - return true; - } - - updateValue = e => this.setState({ inputValue: e.target.value }); - - remove = index => this.setState({ - todos: this.state.todos.filter((el, i) => index !== i), - }); - - drawTodos = todos => todos.map((el, key) => - this.remove(key)}>{el} - ); - - render() { - const { inputValue, todos } = this.state; - return ( -
- - - {this.drawTodos(todos)} - -
- ); - } -} +const drawTodos = (tasks, actions) => tasks.map((el, key) => + {el.title} +); + +const TodoContainer = ({ store, actions }) => +
+ + + {drawTodos(store.state, actions)} + +
+; + +Todo.propTypes = { + store: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, +}; + +export default TodoContainer; diff --git a/src/client/components/TodoContainer/index.js b/src/client/components/TodoContainer/index.js new file mode 100644 index 0000000..8e08847 --- /dev/null +++ b/src/client/components/TodoContainer/index.js @@ -0,0 +1,41 @@ +import React, { PropTypes } from 'react'; +import styled from 'styled-components'; + +import CreateTodoForm from '../CreateTodoForm'; +import TodoEl from '../TodoEl'; + +const TodoList = styled.ul` + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 0; + margin: 0; +`; + +const drawTodos = (todos, actions, dispatch) => todos.map(todo => + + {todo.title} + +); + +const TodoContainer = ({ store, actions }) => +
+ + + {drawTodos(store.state, actions, store.dispatch.bind(store))} + +
+; + +TodoContainer.propTypes = { + store: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, +}; + +export default TodoContainer; diff --git a/src/client/components/TodoEl/index.js b/src/client/components/TodoEl/index.js index 8e4017d..23853fc 100644 --- a/src/client/components/TodoEl/index.js +++ b/src/client/components/TodoEl/index.js @@ -2,49 +2,81 @@ import React, { PropTypes } from 'react'; import styled from 'styled-components'; import colors from '../../colors.json'; -const TodoLi = styled.li` +const TodoBlock = styled.li` list-style: none; - width: 90%; - height: 3em; font-family: Roboto, arial, verdana; background-color: white; - margin-top: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; + margin: 2em; + border-radius: 2px; `; const RemoveButton = styled.button` border: none; - background-color: ${colors.blueGrey}; width: 4em; height: 100%; cursor: pointer; transition: all .2s; - color: white; font-size: 20px; - + background-color: white; &:hover { - background-color: white; - color: ${colors.blueGrey}; + background-color: ${colors.blueGrey}; + color: white; } `; -const TodoText = styled.span` - margin-left: 1em; +const TaskContainer = styled.li` + list-style: none; + display: flex; + justify-content: space-between; + align-items: center; + transition: all .2s; `; -const todoEl = ({ children, onClick }) => - - {children} - - +const TasksList = styled.ul` + display: flex; + flex-direction: column; + padding: 0; + margin-top: 1em; +`; + +const TodoTitle = styled.span` + text-align: center; + margin: 1em 0; +`; + +const TaskTitle = styled.span` + margin: 0 .5em; +`; + +const drawTasks = (tasks, dispatch, actions, todoID) => tasks.map(task => + + {task.title} + dispatch( + actions.todo.removeTask({ taskID: task.id, todoID }) + )} + > + ✖ + + +); + +const TodoEl = ({ children, tasks, dispatch, actions, todoID }) => + + {children} + {drawTasks(tasks, dispatch, actions, todoID)} + ; -todoEl.propTypes = { +TodoEl.propTypes = { children: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, + tasks: PropTypes.array.isRequired, + dispatch: PropTypes.func.isRequired, + actions: PropTypes.object.isRequired, + todoID: PropTypes.number.isRequired, }; -export default todoEl; +export default TodoEl; diff --git a/src/client/index.js b/src/client/index.js index 84ecaca..bf73e65 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -1,6 +1,45 @@ import React from 'react'; import { render } from 'react-dom'; import App from './components/App'; +import actions from './actions'; -console.log('mounting react app ...'); // eslint-disable-line no-console -render(, document.getElementById('__TODO__')); +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(); + }, +}; + +console.log('mounting react app ...'); + +render(, document.getElementById('__TODO__')); From 3d138e7261ed24f6028f7d6450fac0ffd9c8e775 Mon Sep 17 00:00:00 2001 From: Raphael LE MINOR Date: Wed, 4 Jan 2017 22:23:09 +0100 Subject: [PATCH 4/7] add update and remove seem ok --- package.json | 10 +++ public/index.html | 2 +- src/client/actions/todos.js | 43 +++++++++- src/client/components/AddTask/index.js | 68 +++++++++++++++ src/client/components/AddTodo/index.js | 75 +++++++++++++++++ src/client/components/App/index.js | 8 +- src/client/components/Button/index.js | 21 +++-- src/client/components/Container/index.js | 7 ++ src/client/components/CreateTodoForm/index.js | 50 ----------- .../components/FullHeightContainer/index.js | 7 ++ src/client/components/Header/index.js | 17 +--- src/client/components/ModalInput/index.js | 9 ++ src/client/components/RemoveTodo/index.js | 47 +++++++++++ src/client/components/Todo/index.js | 33 -------- src/client/components/TodoContainer/index.js | 12 +-- src/client/components/TodoEl/index.js | 83 +++++++++++++------ src/client/components/TodoInput/index.js | 12 +++ src/client/components/UpdateTask/index.js | 75 +++++++++++++++++ src/client/components/UpdateTodo/index.js | 75 +++++++++++++++++ src/client/index.js | 1 + 20 files changed, 508 insertions(+), 147 deletions(-) create mode 100644 src/client/components/AddTask/index.js create mode 100644 src/client/components/AddTodo/index.js create mode 100644 src/client/components/Container/index.js delete mode 100644 src/client/components/CreateTodoForm/index.js create mode 100644 src/client/components/FullHeightContainer/index.js create mode 100644 src/client/components/ModalInput/index.js create mode 100644 src/client/components/RemoveTodo/index.js delete mode 100644 src/client/components/Todo/index.js create mode 100644 src/client/components/TodoInput/index.js create mode 100644 src/client/components/UpdateTask/index.js create mode 100644 src/client/components/UpdateTodo/index.js diff --git a/package.json b/package.json index 7bf4a6b..a1aae9b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "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", @@ -32,6 +35,7 @@ "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", @@ -55,6 +59,12 @@ ] }, "babel": { + "plugins": [ + [ + "import", + { "libraryName": "antd", "style": "css" } + ] + ], "presets": [ [ "latest", diff --git a/public/index.html b/public/index.html index a69e7bc..7be6672 100644 --- a/public/index.html +++ b/public/index.html @@ -8,7 +8,7 @@ outline: none; } body { - background-color: #F0F0F0; + background-color: #C0C0C0; } diff --git a/src/client/actions/todos.js b/src/client/actions/todos.js index 06df963..6dc81c0 100644 --- a/src/client/actions/todos.js +++ b/src/client/actions/todos.js @@ -29,6 +29,12 @@ export const createTodo = title => (state) => { export const removeTodo = id => state => state.filter(el => el.id !== id); +/* +* payload = { +* title: String, +* todoID: Number +* } +*/ export const addTask = payload => state => state.map((todo) => { if (todo.id === payload.todoID) { const newTask = { @@ -42,7 +48,10 @@ export const addTask = payload => state => state.map((todo) => { }); /* -* seems ok +* payload = { +* todoID: Number, +* taskID: Number, +* } */ export const removeTask = payload => state => state.map((todo) => { if (todo.id === payload.todoID) { @@ -52,14 +61,40 @@ export const removeTask = payload => state => state.map((todo) => { return todo; }); +/* +* payload = { +* todoID: Number, +* title: String (optional), +* checked: Boolean (optional), +* } +*/ +export const updateTodo = payload => state => state.map((todo) => { + if (todo.id === payload.todoID) { + return { + ...todo, + title: payload.title || todo.title, + checked: payload.checked || todo.checked, + }; + } + return todo; +}); + +/* +* payload = { +* todoID: Number, +* taskID: Number, +* title: String (optional), +* checked: Boolean (optional), +* } +*/ export const updateTask = payload => state => state.map((todo) => { if (todo.id === payload.todoID) { const newTasks = todo.tasks.map((task) => { if (task.id === payload.taskID) { return { - title: payload.title, - checked: payload.checked, - id: task.id, + ...task, + title: payload.title || task.title, + checked: payload.checked !== undefined ? payload.checked : task.checked, }; } return task; diff --git a/src/client/components/AddTask/index.js b/src/client/components/AddTask/index.js new file mode 100644 index 0000000..adae8b3 --- /dev/null +++ b/src/client/components/AddTask/index.js @@ -0,0 +1,68 @@ +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 { dispatch, actions, todoID } = this.props; + + dispatch(actions.todo.addTask({ title: value, todoID })); + this.setState({ value: '', visible: false }); + } + + render() { + const { visible, value } = this.state; + return ( + + + Cancel, + , + ]} + > + + + + ); + } +} + +AddTask.propTypes = { + dispatch: PropTypes.func.isRequired, + actions: PropTypes.object.isRequired, + todoID: 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..40ef7f6 --- /dev/null +++ b/src/client/components/AddTodo/index.js @@ -0,0 +1,75 @@ +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 { store, actions } = this.props; + const { dispatch } = store; + const { value } = this.state; + const bDispatch = dispatch.bind(store); + + if (!value) return false; + + bDispatch(actions.todo.createTodo(value)); + this.setState({ value: '' }); + return true; + } + + render() { + const { value } = this.state; + return ( + + + + + ); + } +} + +AddTodo.propTypes = { + store: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, +}; diff --git a/src/client/components/App/index.js b/src/client/components/App/index.js index e382488..f158822 100644 --- a/src/client/components/App/index.js +++ b/src/client/components/App/index.js @@ -1,13 +1,7 @@ import React, { PropTypes } from 'react'; -import styled from 'styled-components'; import Header from '../Header'; import TodoContainer from '../TodoContainer'; - -export const Title = styled.h1` - font-size: 1.5em; - text-align: center; - color: palevioletred; -`; +import MainContainer from '../Container'; export default class App extends React.Component { componentDidMount() { 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 }) => + NO, + , + ]} + > + ARE YOU SURE ? + + + ); + } +} + +RemoveTodo.propTypes = { + dispatch: PropTypes.func.isRequired, + actions: PropTypes.object.isRequired, + todoID: PropTypes.number.isRequired, +}; diff --git a/src/client/components/Todo/index.js b/src/client/components/Todo/index.js deleted file mode 100644 index 9fb4c4c..0000000 --- a/src/client/components/Todo/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { PropTypes } from 'react'; -import styled from 'styled-components'; - -import CreateTodoForm from '../CreateTodoForm'; -import TodoEl from '../TodoEl'; - -const TodoList = styled.ul` - display: flex; - flex-direction: column; - align-items: center; - padding: 0; - margin: 0; -`; - -const drawTodos = (tasks, actions) => tasks.map((el, key) => - {el.title} -); - -const TodoContainer = ({ store, actions }) => -
- - - {drawTodos(store.state, actions)} - -
-; - -Todo.propTypes = { - store: PropTypes.object.isRequired, - actions: PropTypes.object.isRequired, -}; - -export default TodoContainer; diff --git a/src/client/components/TodoContainer/index.js b/src/client/components/TodoContainer/index.js index 8e08847..70b590a 100644 --- a/src/client/components/TodoContainer/index.js +++ b/src/client/components/TodoContainer/index.js @@ -1,15 +1,17 @@ import React, { PropTypes } from 'react'; import styled from 'styled-components'; -import CreateTodoForm from '../CreateTodoForm'; +import AddTodo from '../AddTodo'; +import MainContainer from '../Container'; import TodoEl from '../TodoEl'; const TodoList = styled.ul` display: flex; flex-direction: row; flex-wrap: wrap; - padding: 0; + align-items: flex-start; margin: 0; + padding: 0; `; const drawTodos = (todos, actions, dispatch) => todos.map(todo => @@ -25,12 +27,12 @@ const drawTodos = (todos, actions, dispatch) => todos.map(todo => ); const TodoContainer = ({ store, actions }) => -
- + + {drawTodos(store.state, actions, store.dispatch.bind(store))} -
+ ; TodoContainer.propTypes = { diff --git a/src/client/components/TodoEl/index.js b/src/client/components/TodoEl/index.js index 23853fc..378b0c7 100644 --- a/src/client/components/TodoEl/index.js +++ b/src/client/components/TodoEl/index.js @@ -1,6 +1,11 @@ import React, { PropTypes } from 'react'; import styled from 'styled-components'; -import colors from '../../colors.json'; + +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; @@ -13,20 +18,6 @@ const TodoBlock = styled.li` border-radius: 2px; `; -const RemoveButton = 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; - } -`; - const TaskContainer = styled.li` list-style: none; display: flex; @@ -39,34 +30,74 @@ const TasksList = styled.ul` display: flex; flex-direction: column; padding: 0; - margin-top: 1em; `; const TodoTitle = styled.span` - text-align: center; - margin: 1em 0; + 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, dispatch, actions, todoID) => tasks.map(task => - {task.title} - dispatch( - actions.todo.removeTask({ taskID: task.id, todoID }) - )} + dispatch(actions.todo.updateTask({ + todoID, + taskID: task.id, + checked: !task.checked, + }))} > - ✖ - + {task.title} + + + + + ); const TodoEl = ({ children, tasks, dispatch, actions, todoID }) => - {children} + + {children} + + + + {drawTasks(tasks, dispatch, actions, todoID)} ; 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..272e2c3 --- /dev/null +++ b/src/client/components/UpdateTask/index.js @@ -0,0 +1,75 @@ +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.value, + } + + componentWillReceiveProps = (newProps) => { + this.setState({ value: newProps.value }); + } + + updateTask = () => { + const { value } = this.state; + const { dispatch, actions, taskID, todoID } = this.props; + + if (!value || value.length > 1000) return false; + dispatch(actions.todo.updateTask({ todoID, taskID, title: 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.updateTask(); + } + } + + render() { + const { value, visible } = this.state; + return ( +
+ + Cancel, + , + ]} + > + + +
+ ); + } +} + +UpdateTask.propTypes = { + value: PropTypes.string.isRequired, + actions: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + taskID: PropTypes.number.isRequired, + todoID: PropTypes.number.isRequired, +}; diff --git a/src/client/components/UpdateTodo/index.js b/src/client/components/UpdateTodo/index.js new file mode 100644 index 0000000..e32e3c1 --- /dev/null +++ b/src/client/components/UpdateTodo/index.js @@ -0,0 +1,75 @@ +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.value, + } + + componentWillReceiveProps = (newProps) => { + this.setState({ value: newProps.value }); + } + + updateTodo = () => { + const { value } = this.state; + const { dispatch, actions, todoID } = this.props; + + if (!value || value.length > 1000) return false; + dispatch(actions.todo.updateTodo({ todoID, title: 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 = { + value: PropTypes.string.isRequired, + actions: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + todoID: PropTypes.number.isRequired, +}; diff --git a/src/client/index.js b/src/client/index.js index bf73e65..cf8e304 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -1,6 +1,7 @@ import React from 'react'; import { render } from 'react-dom'; import App from './components/App'; +import 'antd/dist/antd.css'; import actions from './actions'; const state = [ From 8c59e0bf33a9434ac2f0452ab5ed1237097b4c02 Mon Sep 17 00:00:00 2001 From: Raphael LE MINOR Date: Tue, 10 Jan 2017 09:51:11 +0100 Subject: [PATCH 5/7] rebase --- .../components/AddTask/AddTodo/index.js | 75 +++++++++++++++++++ src/client/store.js | 35 +++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/client/components/AddTask/AddTodo/index.js create mode 100644 src/client/store.js diff --git a/src/client/components/AddTask/AddTodo/index.js b/src/client/components/AddTask/AddTodo/index.js new file mode 100644 index 0000000..40ef7f6 --- /dev/null +++ b/src/client/components/AddTask/AddTodo/index.js @@ -0,0 +1,75 @@ +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 { store, actions } = this.props; + const { dispatch } = store; + const { value } = this.state; + const bDispatch = dispatch.bind(store); + + if (!value) return false; + + bDispatch(actions.todo.createTodo(value)); + this.setState({ value: '' }); + return true; + } + + render() { + const { value } = this.state; + return ( + + + + + ); + } +} + +AddTodo.propTypes = { + store: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, +}; diff --git a/src/client/store.js b/src/client/store.js new file mode 100644 index 0000000..3d9728a --- /dev/null +++ b/src/client/store.js @@ -0,0 +1,35 @@ +import React, { PropTypes } from 'react'; + +const storeGen = { + listeners: [], + listen(cb) { this.listeners.push(cb); }, + callListeners() { this.listeners.forEach(cb => cb()); }, + dispatch(action) { + this.state = action(this.state); + this.callListeners(); + }, +}; + +export const createStore = state => ({ ...storeGen, state }); + +export class Provider extends React.Component { + + componentWillMount() { + const { store } = this.props; + store.listen(() => this.forceUpdate()); + } + + render() { + const { children, store, actions } = this.props; + return React.cloneElement( + React.Children.only(children), + { store, actions }, + ); + } +} + +Provider.propTypes = { + store: PropTypes.object.isRequired, + actions: PropTypes.object.isRequired, + children: PropTypes.object.isRequired, +}; From 9c1ee989e7383c38c9cbf202ce0757b43ce0e3f2 Mon Sep 17 00:00:00 2001 From: Raphael LE MINOR Date: Thu, 12 Jan 2017 15:58:23 +0100 Subject: [PATCH 6/7] add test --- package.json | 11 ++-- src/client/components/App/__test__/index.js | 59 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a1aae9b..2195754 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,6 @@ "styled-components": "^1.2.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", @@ -39,7 +36,9 @@ "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", @@ -48,6 +47,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", @@ -62,7 +62,10 @@ "plugins": [ [ "import", - { "libraryName": "antd", "style": "css" } + { + "libraryName": "antd", + "style": "css" + } ] ], "presets": [ diff --git a/src/client/components/App/__test__/index.js b/src/client/components/App/__test__/index.js index 067d243..9eaf40a 100644 --- a/src/client/components/App/__test__/index.js +++ b/src/client/components/App/__test__/index.js @@ -1,13 +1,64 @@ import React from 'react'; import chai from 'chai'; import { shallow } from 'enzyme'; -import App, { Title } from '..'; + +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('[UT] ', () => { - it('should render a ', () => { - expect(shallow(<App />).find(Title)).to.have.length(1); +describe('<App />', () => { + const app = shallow(<App store={store} actions={actions} />); + it('should render a <Header />', () => { + expect(app.find(Header)).to.have.length(1); + }); + it('should render a <TodoContainer />', () => { + 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); }); }); From 92b8bc91d1a6fc3a5d63d06058694a1cba96bd67 Mon Sep 17 00:00:00 2001 From: Raphael LE MINOR <rle-mino@e2r3p6.42.fr> Date: Tue, 17 Jan 2017 19:48:26 +0100 Subject: [PATCH 7/7] add redux, redux thunk, now all data come from the api. remove, add, update are now handled by the api --- package.json | 9 +- src/client/actions/index.js | 2 + src/client/actions/tasks.js | 39 ++++++ src/client/actions/todos.js | 128 +++++------------- src/client/apiURI.js | 1 + .../components/AddTask/AddTodo/index.js | 75 ---------- src/client/components/AddTask/index.js | 7 +- src/client/components/AddTodo/index.js | 7 +- src/client/components/App/index.js | 26 ---- src/client/components/RemoveTodo/index.js | 7 +- src/client/components/TodoContainer/index.js | 22 +-- src/client/components/TodoEl/index.js | 44 +++--- src/client/components/UpdateTask/index.js | 14 +- src/client/components/UpdateTodo/index.js | 13 +- .../App/__test__/index.js | 0 src/client/containers/App/index.js | 30 ++++ src/client/index.js | 50 ++----- src/client/initialState.js | 26 ++++ src/client/reducers/index.js | 8 ++ src/client/reducers/tasks.js | 27 ++++ src/client/reducers/todos.js | 27 ++++ src/client/requestJSON.js | 18 +++ src/client/store.js | 64 +++++---- 23 files changed, 307 insertions(+), 337 deletions(-) create mode 100644 src/client/actions/tasks.js create mode 100644 src/client/apiURI.js delete mode 100644 src/client/components/AddTask/AddTodo/index.js delete mode 100644 src/client/components/App/index.js rename src/client/{components => containers}/App/__test__/index.js (100%) create mode 100644 src/client/containers/App/index.js create mode 100644 src/client/initialState.js create mode 100644 src/client/reducers/index.js create mode 100644 src/client/reducers/tasks.js create mode 100644 src/client/reducers/todos.js create mode 100644 src/client/requestJSON.js diff --git a/package.json b/package.json index 2195754..09bff37 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,13 @@ "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": { "babel-cli": "^6.18.0", diff --git a/src/client/actions/index.js b/src/client/actions/index.js index ce942e1..b9f0800 100644 --- a/src/client/actions/index.js +++ b/src/client/actions/index.js @@ -1,5 +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 index 6dc81c0..076a880 100644 --- a/src/client/actions/todos.js +++ b/src/client/actions/todos.js @@ -1,105 +1,39 @@ -/* -* All the actions, -* state = [todo1, todo2, todo3, ...] -* todo = { -* id: Number, -* title: String, -* checked: boolean, -* tasks: Array -* } -* tasks = { -* id: Number, -* title: String, -* Checked: boolean, -* } -*/ +import requestJSON from '../requestJSON'; +import apiURI from '../apiURI'; -let taID = 9; -let toID = 2; +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 createTodo = title => (state) => { - const newTodo = { - checked: false, - id: toID += 1, - tasks: [], - title, - }; - return [...state, newTodo]; -}; - -export const removeTodo = id => state => state.filter(el => el.id !== id); - -/* -* payload = { -* title: String, -* todoID: Number -* } -*/ -export const addTask = payload => state => state.map((todo) => { - if (todo.id === payload.todoID) { - const newTask = { - title: payload.title, - checked: false, - id: taID += 1, - }; - return { ...todo, tasks: [...todo.tasks, newTask] }; - } - return todo; +export const todoCreated = payload => ({ + type: TODO_CREATED, + payload, }); -/* -* payload = { -* todoID: Number, -* taskID: Number, -* } -*/ -export const removeTask = payload => state => state.map((todo) => { - if (todo.id === payload.todoID) { - const newTasks = todo.tasks.filter(task => task.id !== payload.taskID); - return { ...todo, tasks: newTasks }; - } - return todo; +export const todoUpdated = payload => ({ + type: TODO_UPDATED, + payload, }); -/* -* payload = { -* todoID: Number, -* title: String (optional), -* checked: Boolean (optional), -* } -*/ -export const updateTodo = payload => state => state.map((todo) => { - if (todo.id === payload.todoID) { - return { - ...todo, - title: payload.title || todo.title, - checked: payload.checked || todo.checked, - }; - } - return todo; +export const todoRemoved = payload => ({ + type: TODO_REMOVED, + payload, }); -/* -* payload = { -* todoID: Number, -* taskID: Number, -* title: String (optional), -* checked: Boolean (optional), -* } -*/ -export const updateTask = payload => state => state.map((todo) => { - if (todo.id === payload.todoID) { - const newTasks = todo.tasks.map((task) => { - if (task.id === payload.taskID) { - return { - ...task, - title: payload.title || task.title, - checked: payload.checked !== undefined ? payload.checked : task.checked, - }; - } - return task; - }); - return { ...todo, tasks: newTasks }; - } - return todo; -}); +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/components/AddTask/AddTodo/index.js b/src/client/components/AddTask/AddTodo/index.js deleted file mode 100644 index 40ef7f6..0000000 --- a/src/client/components/AddTask/AddTodo/index.js +++ /dev/null @@ -1,75 +0,0 @@ -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 { store, actions } = this.props; - const { dispatch } = store; - const { value } = this.state; - const bDispatch = dispatch.bind(store); - - if (!value) return false; - - bDispatch(actions.todo.createTodo(value)); - this.setState({ value: '' }); - return true; - } - - render() { - const { value } = this.state; - return ( - <FormContainer onSubmit={this.addTodo}> - <TodoInput - placeholder="Enter your todo here" - type="text" - name="todo" - autoComplete="off" - value={value} - onChange={this.handleChange} - /> - <TodoSubmit type="submit" value="Save your todo" /> - </FormContainer> - ); - } -} - -AddTodo.propTypes = { - store: PropTypes.object.isRequired, - actions: PropTypes.object.isRequired, -}; diff --git a/src/client/components/AddTask/index.js b/src/client/components/AddTask/index.js index adae8b3..88b15ef 100644 --- a/src/client/components/AddTask/index.js +++ b/src/client/components/AddTask/index.js @@ -26,9 +26,9 @@ export default class AddTask extends React.Component { saveTask = () => { const { value } = this.state; - const { dispatch, actions, todoID } = this.props; + const { actions, listId } = this.props; - dispatch(actions.todo.addTask({ title: value, todoID })); + actions.task.add({ description: value, isCompleted: false, listId }); this.setState({ value: '', visible: false }); } @@ -62,7 +62,6 @@ export default class AddTask extends React.Component { } AddTask.propTypes = { - dispatch: PropTypes.func.isRequired, actions: PropTypes.object.isRequired, - todoID: PropTypes.number.isRequired, + listId: PropTypes.number.isRequired, }; diff --git a/src/client/components/AddTodo/index.js b/src/client/components/AddTodo/index.js index 40ef7f6..3b32f62 100644 --- a/src/client/components/AddTodo/index.js +++ b/src/client/components/AddTodo/index.js @@ -39,14 +39,12 @@ export default class AddTodo extends React.Component { addTodo = (e) => { e.preventDefault(); - const { store, actions } = this.props; - const { dispatch } = store; + const { actions } = this.props; const { value } = this.state; - const bDispatch = dispatch.bind(store); if (!value) return false; - bDispatch(actions.todo.createTodo(value)); + actions.todo.create({ label: value }); this.setState({ value: '' }); return true; } @@ -70,6 +68,5 @@ export default class AddTodo extends React.Component { } AddTodo.propTypes = { - store: PropTypes.object.isRequired, actions: PropTypes.object.isRequired, }; diff --git a/src/client/components/App/index.js b/src/client/components/App/index.js deleted file mode 100644 index f158822..0000000 --- a/src/client/components/App/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import React, { PropTypes } from 'react'; -import Header from '../Header'; -import TodoContainer from '../TodoContainer'; -import MainContainer from '../Container'; - -export default class App extends React.Component { - componentDidMount() { - const { store } = this.props; - store.listen(() => this.forceUpdate()); - } - - render() { - const { store, actions } = this.props; - return ( - <MainContainer> - <Header /> - <TodoContainer store={store} actions={actions} /> - </MainContainer> - ); - } -} - -App.propTypes = { - store: PropTypes.object.isRequired, - actions: PropTypes.object.isRequired, -}; diff --git a/src/client/components/RemoveTodo/index.js b/src/client/components/RemoveTodo/index.js index 64d83eb..617ebf6 100644 --- a/src/client/components/RemoveTodo/index.js +++ b/src/client/components/RemoveTodo/index.js @@ -14,9 +14,9 @@ export default class RemoveTodo extends React.Component { showModal = () => this.setState({ visible: true }) removeTodo = () => { - const { dispatch, actions, todoID } = this.props; + const { actions, listId } = this.props; - dispatch(actions.todo.removeTodo(todoID)); + actions.todo.remove(listId); } render() { @@ -41,7 +41,6 @@ export default class RemoveTodo extends React.Component { } RemoveTodo.propTypes = { - dispatch: PropTypes.func.isRequired, actions: PropTypes.object.isRequired, - todoID: PropTypes.number.isRequired, + listId: PropTypes.number.isRequired, }; diff --git a/src/client/components/TodoContainer/index.js b/src/client/components/TodoContainer/index.js index 70b590a..621f280 100644 --- a/src/client/components/TodoContainer/index.js +++ b/src/client/components/TodoContainer/index.js @@ -14,29 +14,31 @@ const TodoList = styled.ul` padding: 0; `; -const drawTodos = (todos, actions, dispatch) => todos.map(todo => +const linkTasks = (id, tasks) => tasks.filter(task => task.listId === id); + +const drawTodos = (todos, tasks, actions) => todos.map(todo => <TodoEl key={todo.id} - tasks={todo.tasks} + tasks={linkTasks(todo.id, tasks)} actions={actions} - dispatch={dispatch} - todoID={todo.id} + listId={todo.id} > - {todo.title} - </TodoEl> + {todo.label} + </TodoEl>, ); -const TodoContainer = ({ store, actions }) => +const TodoContainer = ({ todos, tasks, actions }) => <MainContainer> - <AddTodo store={store} actions={actions} /> + <AddTodo todos={todos} actions={actions} /> <TodoList> - {drawTodos(store.state, actions, store.dispatch.bind(store))} + {drawTodos(todos, tasks, actions)} </TodoList> </MainContainer> ; TodoContainer.propTypes = { - store: PropTypes.object.isRequired, + todos: PropTypes.array.isRequired, + tasks: PropTypes.array.isRequired, actions: PropTypes.object.isRequired, }; diff --git a/src/client/components/TodoEl/index.js b/src/client/components/TodoEl/index.js index 378b0c7..fcafa63 100644 --- a/src/client/components/TodoEl/index.js +++ b/src/client/components/TodoEl/index.js @@ -59,55 +59,43 @@ const TaskControls = styled.div` display: flex; `; -const drawTasks = (tasks, dispatch, actions, todoID) => tasks.map(task => +const drawTasks = (tasks, actions) => tasks.map(task => <TaskContainer key={task.id}> <TaskTitle - checked={task.checked} - onClick={() => dispatch(actions.todo.updateTask({ - todoID, - taskID: task.id, - checked: !task.checked, - }))} + checked={task.isCompleted} + onClick={() => actions.task.update({ + ...task, + isCompleted: !task.isCompleted, + })} > - {task.title} + {task.description} </TaskTitle> <TaskControls> - <UpdateTask - dispatch={dispatch} - actions={actions} - todoID={todoID} - taskID={task.id} - value={task.title} - /> - <Button - onClick={() => dispatch( - actions.todo.removeTask({ taskID: task.id, todoID }) - )} - > + <UpdateTask actions={actions} task={task} /> + <Button onClick={() => actions.task.remove(task.id)}> ✖ </Button> </TaskControls> - </TaskContainer> + </TaskContainer>, ); -const TodoEl = ({ children, tasks, dispatch, actions, todoID }) => +const TodoEl = ({ children, tasks, actions, listId }) => <TodoBlock> <TopTodo> <TodoTitle>{children}</TodoTitle> - <AddTask dispatch={dispatch} actions={actions} todoID={todoID} /> - <UpdateTodo dispatch={dispatch} actions={actions} todoID={todoID} value={children} /> - <RemoveTodo dispatch={dispatch} actions={actions} todoID={todoID} /> + <AddTask actions={actions} listId={listId} /> + <UpdateTodo actions={actions} listId={listId} label={children} /> + <RemoveTodo actions={actions} listId={listId} /> </TopTodo> - <TasksList>{drawTasks(tasks, dispatch, actions, todoID)}</TasksList> + <TasksList>{drawTasks(tasks, actions)}</TasksList> </TodoBlock> ; TodoEl.propTypes = { children: PropTypes.string.isRequired, tasks: PropTypes.array.isRequired, - dispatch: PropTypes.func.isRequired, actions: PropTypes.object.isRequired, - todoID: PropTypes.number.isRequired, + listId: PropTypes.number.isRequired, }; export default TodoEl; diff --git a/src/client/components/UpdateTask/index.js b/src/client/components/UpdateTask/index.js index 272e2c3..be226e3 100644 --- a/src/client/components/UpdateTask/index.js +++ b/src/client/components/UpdateTask/index.js @@ -7,19 +7,20 @@ import ModalInput from '../ModalInput'; export default class UpdateTask extends React.Component { state = { visible: false, - value: this.props.value, + value: this.props.task.description, } componentWillReceiveProps = (newProps) => { - this.setState({ value: newProps.value }); + this.setState({ value: newProps.task.description }); } updateTask = () => { const { value } = this.state; - const { dispatch, actions, taskID, todoID } = this.props; + const { actions, task } = this.props; + const { id, isCompleted, listId } = task; if (!value || value.length > 1000) return false; - dispatch(actions.todo.updateTask({ todoID, taskID, title: value })); + actions.task.update({ listId, id, description: value, isCompleted }); this.setState({ visible: false }); return (true); } @@ -67,9 +68,6 @@ export default class UpdateTask extends React.Component { } UpdateTask.propTypes = { - value: PropTypes.string.isRequired, actions: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - taskID: PropTypes.number.isRequired, - todoID: PropTypes.number.isRequired, + task: PropTypes.object.isRequired, }; diff --git a/src/client/components/UpdateTodo/index.js b/src/client/components/UpdateTodo/index.js index e32e3c1..1b2ecf3 100644 --- a/src/client/components/UpdateTodo/index.js +++ b/src/client/components/UpdateTodo/index.js @@ -8,19 +8,19 @@ import ModalInput from '../ModalInput'; export default class UpdateTodo extends React.Component { state = { visible: false, - value: this.props.value, + value: this.props.label, } componentWillReceiveProps = (newProps) => { - this.setState({ value: newProps.value }); + this.setState({ value: newProps.label }); } updateTodo = () => { const { value } = this.state; - const { dispatch, actions, todoID } = this.props; + const { actions, listId } = this.props; if (!value || value.length > 1000) return false; - dispatch(actions.todo.updateTodo({ todoID, title: value })); + actions.todo.update({ id: listId, label: value }); this.setState({ visible: false }); return (true); } @@ -68,8 +68,7 @@ export default class UpdateTodo extends React.Component { } UpdateTodo.propTypes = { - value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, actions: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - todoID: PropTypes.number.isRequired, + listId: PropTypes.number.isRequired, }; diff --git a/src/client/components/App/__test__/index.js b/src/client/containers/App/__test__/index.js similarity index 100% rename from src/client/components/App/__test__/index.js rename to src/client/containers/App/__test__/index.js 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 }) => + <MainContainer> + <Header /> + <TodoContainer todos={todos} tasks={tasks} actions={actions} /> + </MainContainer> +; + +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 cf8e304..f39445f 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -1,46 +1,16 @@ import React from 'react'; import { render } from 'react-dom'; -import App from './components/App'; import 'antd/dist/antd.css'; -import actions from './actions'; +import { Provider } from 'react-redux'; +import App from './containers/App'; +import store from './store'; -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 Root = ( + <Provider store={store}> + <App /> + </Provider> +); -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(); - }, -}; +console.log('mounting react app ...'); // eslint-disable-line no-console -console.log('mounting react app ...'); - -render(<App store={store} actions={actions} />, document.getElementById('__TODO__')); +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 index 3d9728a..fa5b250 100644 --- a/src/client/store.js +++ b/src/client/store.js @@ -1,35 +1,41 @@ -import React, { PropTypes } from 'react'; +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 storeGen = { - listeners: [], - listen(cb) { this.listeners.push(cb); }, - callListeners() { this.listeners.forEach(cb => cb()); }, - dispatch(action) { - this.state = action(this.state); - this.callListeners(); - }, -}; +const logger = createLogger(); -export const createStore = state => ({ ...storeGen, state }); +const generateStore = (mode) => { + if (mode === 'development') { + return createStore( + reducers, + initialState, + applyMiddleware( + logger, + thunkMiddleware, + ), + ); + } + return createStore( + reducers, + initialState, + applyMiddleware( + thunkMiddleware, + ), + ); +}; -export class Provider extends React.Component { +const store = generateStore(process.env.NODE_ENV); - componentWillMount() { - const { store } = this.props; - store.listen(() => this.forceUpdate()); - } +getInitialTodos((data) => { + store.dispatch({ type: INITIAL_TODOS_LOADED, payload: data }); +}); - render() { - const { children, store, actions } = this.props; - return React.cloneElement( - React.Children.only(children), - { store, actions }, - ); - } -} +getInitialTasks((data) => { + store.dispatch({ type: INITIAL_TASKS_LOADED, payload: data }); +}); -Provider.propTypes = { - store: PropTypes.object.isRequired, - actions: PropTypes.object.isRequired, - children: PropTypes.object.isRequired, -}; +export default store;