From 7f55b136a16b150684f9c8ce816ef014118c648f Mon Sep 17 00:00:00 2001 From: Kai Yuan Neo Date: Tue, 18 Feb 2020 00:50:26 +0800 Subject: [PATCH] update todo list to support async requests to fetch todos --- backend/package-lock.json | 14 +++++++ backend/package.json | 1 + backend/src/app.js | 2 + backend/src/bin/www | 2 +- backend/src/controllers/todoController.js | 11 +++++- frontend/package-lock.json | 18 +++++++++ frontend/package.json | 6 ++- frontend/src/actions/index.js | 24 ++++++++++++ frontend/src/{ => components}/App.test.js | 0 frontend/src/components/Todo.js | 6 +-- frontend/src/components/TodoList.js | 25 ++++++++---- frontend/src/configureStore.js | 15 ++++++++ frontend/src/containers/VisibleTodoList.js | 5 ++- frontend/src/index.js | 9 +++-- frontend/src/reducers/todos.js | 44 ++++++++++++++++------ 15 files changed, 150 insertions(+), 32 deletions(-) rename frontend/src/{ => components}/App.test.js (100%) create mode 100644 frontend/src/configureStore.js diff --git a/backend/package-lock.json b/backend/package-lock.json index f0d688b..624acd6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1508,6 +1508,15 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -3568,6 +3577,11 @@ "path-key": "^2.0.0" } }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index b03a509..1e0cb76 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "@babel/preset-env": "^7.8.4", "cookie-parser": "~1.4.4", "core-js": "^3.6.4", + "cors": "^2.8.5", "debug": "~2.6.9", "express": "~4.16.1", "http-errors": "~1.6.3", diff --git a/backend/src/app.js b/backend/src/app.js index 53e7d0f..abc6f88 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -1,5 +1,6 @@ import cookieParser from "cookie-parser"; import "core-js/stable"; +import cors from 'cors' import express from "express"; import createError from "http-errors"; import logger from "morgan"; @@ -10,6 +11,7 @@ import todoRoutes from './routes/todoRoutes'; var app = express(); app.use(logger("dev")); +app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); diff --git a/backend/src/bin/www b/backend/src/bin/www index 352bba3..f078875 100755 --- a/backend/src/bin/www +++ b/backend/src/bin/www @@ -12,7 +12,7 @@ var http = require('http'); * Get port from environment and store in Express. */ -var port = normalizePort(process.env.PORT || '3000'); +var port = normalizePort(process.env.PORT || '4000'); app.set('port', port); /** diff --git a/backend/src/controllers/todoController.js b/backend/src/controllers/todoController.js index c900f60..25080bb 100644 --- a/backend/src/controllers/todoController.js +++ b/backend/src/controllers/todoController.js @@ -9,7 +9,7 @@ export const createTodo = async (req, res) => { value, completed: false }) - res.send({ todoId: todo.id }); + res.json({ todoId: todo.id }); }; /** @@ -17,7 +17,14 @@ export const createTodo = async (req, res) => { */ export const retrieveTodos = async (req, res) => { const todos = await Todo.findAll(); - res.send(todos); + // Strip todos so that we send minimal information to client + const strippedTodos = todos.map(todo => ({ + id: todo.id, + value: todo.value, + completed: todo.completed + })); + console.log(strippedTodos); + res.json(strippedTodos); }; /** diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7122e84..349237c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3789,6 +3789,11 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-diff": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz", + "integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ=" + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -10599,6 +10604,19 @@ "symbol-observable": "^1.2.0" } }, + "redux-logger": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz", + "integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=", + "requires": { + "deep-diff": "^0.3.5" + } + }, + "redux-thunk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz", + "integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==" + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4a33fa4..742decf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,11 +6,15 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.4.0", "@testing-library/user-event": "^7.2.1", + "core-js": "^3.6.4", "react": "^16.12.0", "react-dom": "^16.12.0", "react-redux": "^7.1.3", "react-scripts": "3.3.1", - "redux": "^4.0.5" + "redux": "^4.0.5", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", + "regenerator-runtime": "^0.13.3" }, "scripts": { "start": "react-scripts start", diff --git a/frontend/src/actions/index.js b/frontend/src/actions/index.js index 06bf9c5..a5ba8fd 100644 --- a/frontend/src/actions/index.js +++ b/frontend/src/actions/index.js @@ -5,6 +5,15 @@ export const addTodo = text => ({ text }) +export const requestTodos = () => ({ + type: 'REQUEST_TODOS' +}) + +export const receiveTodos = todos => ({ + type: 'RECEIVE_TODOS', + todos, +}) + export const setVisibilityFilter = filter => ({ type: 'SET_VISIBILITY_FILTER', filter @@ -19,4 +28,19 @@ export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE' +} + +export const fetchTodos = () => { + return dispatch => { + dispatch(requestTodos()) + return fetch('http://localhost:4000/todos/retrieve') + .then(response => { + console.log(response) + return response.json() + }) + .then(todos => { + console.log(todos) + dispatch(receiveTodos(todos)) + }) + } } \ No newline at end of file diff --git a/frontend/src/App.test.js b/frontend/src/components/App.test.js similarity index 100% rename from frontend/src/App.test.js rename to frontend/src/components/App.test.js diff --git a/frontend/src/components/Todo.js b/frontend/src/components/Todo.js index c7fd58e..fc175e0 100644 --- a/frontend/src/components/Todo.js +++ b/frontend/src/components/Todo.js @@ -1,21 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' -const Todo = ({ onClick, completed, text }) => ( +const Todo = ({ onClick, completed, value }) => (
  • - {text} + {value}
  • ) Todo.propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, - text: PropTypes.string.isRequired + value: PropTypes.string.isRequired } export default Todo \ No newline at end of file diff --git a/frontend/src/components/TodoList.js b/frontend/src/components/TodoList.js index 5fa5431..59a898e 100644 --- a/frontend/src/components/TodoList.js +++ b/frontend/src/components/TodoList.js @@ -1,21 +1,30 @@ import React from 'react' import PropTypes from 'prop-types' + import Todo from './Todo' -const TodoList = ({ todos, toggleTodo }) => ( - -) +class TodoList extends React.Component { + componentDidMount() { + this.props.fetchTodos() + } + + render() { + return ( + + ) + } +} TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, - text: PropTypes.string.isRequired + value: PropTypes.string.isRequired }).isRequired ).isRequired, toggleTodo: PropTypes.func.isRequired diff --git a/frontend/src/configureStore.js b/frontend/src/configureStore.js new file mode 100644 index 0000000..8ee142c --- /dev/null +++ b/frontend/src/configureStore.js @@ -0,0 +1,15 @@ +import { createStore, applyMiddleware } from 'redux' +import { createLogger } from 'redux-logger' +import thunkMiddleware from 'redux-thunk' + +import rootReducer from './reducers' + +const loggerMiddleware = createLogger() + +export default function configureStore(preloadedState) { + return createStore( + rootReducer, + preloadedState, + applyMiddleware(thunkMiddleware, loggerMiddleware) + ) +} \ No newline at end of file diff --git a/frontend/src/containers/VisibleTodoList.js b/frontend/src/containers/VisibleTodoList.js index 82006e2..407ae8a 100644 --- a/frontend/src/containers/VisibleTodoList.js +++ b/frontend/src/containers/VisibleTodoList.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux' -import { toggleTodo, VisibilityFilters } from '../actions' +import { fetchTodos, toggleTodo, VisibilityFilters } from '../actions' import TodoList from '../components/TodoList' const getVisibleTodos = (todos, filter) => { @@ -16,10 +16,11 @@ const getVisibleTodos = (todos, filter) => { } const mapStateToProps = state => ({ - todos: getVisibleTodos(state.todos, state.visibilityFilter) + todos: getVisibleTodos(state.todos.todos, state.visibilityFilter) }) const mapDispatchToProps = dispatch => ({ + fetchTodos: () => dispatch(fetchTodos()), toggleTodo: id => dispatch(toggleTodo(id)) }) diff --git a/frontend/src/index.js b/frontend/src/index.js index a49af1c..f4fd50c 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,12 +1,13 @@ +import 'core-js/stable'; import React from 'react'; import { render } from 'react-dom'; -import { Provider } from 'react-redux' -import { createStore } from 'redux' +import { Provider } from 'react-redux'; +import 'regenerator-runtime'; import App from './components/App'; -import rootReducer from './reducers' +import configureStore from './configureStore'; -const store = createStore(rootReducer) +const store = configureStore() render( diff --git a/frontend/src/reducers/todos.js b/frontend/src/reducers/todos.js index 0c7283e..6fccefb 100644 --- a/frontend/src/reducers/todos.js +++ b/frontend/src/reducers/todos.js @@ -1,18 +1,40 @@ -const todos = (state = [], action) => { +const initialState = { + isFetching: false, + todos: [] +} + +const todos = (state = initialState, action) => { switch (action.type) { case 'ADD_TODO': - return [ + return { + ...state, + todos: [ + ...state.todos, + { + id: action.id, + value: action.value, + completed: false + } + ] + } + case 'REQUEST_TODOS': + return { ...state, - { - id: action.id, - text: action.text, - completed: false - } - ] + isFetching: true, + } + case 'RECEIVE_TODOS': + return { + ...state, + isFetching: false, + todos: action.todos + } case 'TOGGLE_TODO': - return state.map(todo => - todo.id === action.id ? { ...todo, completed: !todo.completed } : todo - ) + return { + ...state, + todos: state.todos.map(todo => + todo.id === action.id ? { ...todo, completed: !todo.completed } : todo + ) + } default: return state }