From e3e1efa521ef8216f34e5e86074c46a87bbbbd01 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Fri, 13 Jul 2018 22:38:13 +0300 Subject: [PATCH] animate add todo/filter selector --- app.js | 40 +++--- errors/index.js | 2 + package-lock.json | 146 ++++++++++++++++++---- package.json | 7 +- react/src/components/App.css | 1 - react/src/components/App.js | 4 +- react/src/components/Filters.js | 4 +- react/src/components/Input.js | 7 +- react/src/components/MainView.js | 21 ++++ react/src/components/Selector.js | 100 +++++++++------ react/src/components/Todos.js | 43 ++++--- react/src/containers/InputContainer.js | 6 +- react/src/containers/MainViewContainer.js | 12 ++ react/src/containers/TodosContainer.js | 5 +- 14 files changed, 284 insertions(+), 114 deletions(-) create mode 100644 react/src/components/MainView.js create mode 100644 react/src/containers/MainViewContainer.js diff --git a/app.js b/app.js index c231d1d..f456948 100644 --- a/app.js +++ b/app.js @@ -3,12 +3,12 @@ const express = require('express'); const bodyParser = require('body-parser'); const morgan = require('morgan'); const cors = require('cors'); -const config = require('./config'); -const db = require('./config/db'); const path = require('path'); const hsts = require('hsts'); const compression = require('compression'); const { redirectToHTTPS } = require('express-http-to-https'); +const db = require('./config/db'); +const config = require('./config'); require('./models/TodoList'); require('./models/User'); @@ -74,22 +74,26 @@ app.use((req, res) => { // handle errors app.use((error, req, res, next) => { - switch (error.name) { - case 'ValidationError': - case 'MissingPasswordError': - case 'BadRequest': - case 'BadRequestError': - res.status(400); - break; - case 'AuthenticationError': - case 'UnauthorizedError': - res.status(401); - break; - case 'NotFound': - res.status(404); - break; - default: - res.status(500); + if (error.code) { + res.status(error.code); + } else { + switch (error.name) { + case 'ValidationError': + case 'MissingPasswordError': + case 'BadRequest': + case 'BadRequestError': + res.status(400); + break; + case 'AuthenticationError': + case 'UnauthorizedError': + res.status(401); + break; + case 'NotFound': + res.status(404); + break; + default: + res.status(500); + } } res.json({ success: false, error }); if ( diff --git a/errors/index.js b/errors/index.js index f0ad2e4..b940704 100644 --- a/errors/index.js +++ b/errors/index.js @@ -4,6 +4,7 @@ class NotFoundError extends Error { Error.captureStackTrace(this, NotFoundError); this.name = 'NotFound'; this.text = text; + this.code = 404; } } @@ -13,6 +14,7 @@ class BadRequestError extends Error { Error.captureStackTrace(this, NotFoundError); this.name = 'BadRequest'; this.text = text; + this.code = 400; } } diff --git a/package-lock.json b/package-lock.json index 97d4a3b..8db584e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1688,6 +1688,94 @@ "typedarray": "^0.0.6" } }, + "concurrently": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-3.6.0.tgz", + "integrity": "sha512-6XiIYtYzmGEccNZFkih5JOH92jLA4ulZArAYy5j1uDSdrPLB3KzdE8GW7t2fHPcg9ry2+5LP9IEYzXzxw9lFdA==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "commander": "2.6.0", + "date-fns": "^1.23.0", + "lodash": "^4.5.1", + "read-pkg": "^3.0.0", + "rx": "2.3.24", + "spawn-command": "^0.0.2-1", + "supports-color": "^3.2.3", + "tree-kill": "^1.1.0" + }, + "dependencies": { + "commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -1853,6 +1941,12 @@ "whatwg-url": "^6.4.0" } }, + "date-fns": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", + "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3514,14 +3608,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3536,20 +3628,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3666,8 +3755,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3679,7 +3767,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3694,7 +3781,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3702,14 +3788,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3728,7 +3812,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3809,8 +3892,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3822,7 +3904,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3944,7 +4025,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5482,6 +5562,12 @@ "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -7593,6 +7679,12 @@ "is-promise": "^2.1.0" } }, + "rx": { + "version": "2.3.24", + "resolved": "https://registry.npmjs.org/rx/-/rx-2.3.24.tgz", + "integrity": "sha1-FPlQpCF9fjXapxu8vljv9o6ksrc=", + "dev": true + }, "rx-lite": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", @@ -8001,6 +8093,12 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "spawn-command": { + "version": "0.0.2-1", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", + "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=", + "dev": true + }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -8527,6 +8625,12 @@ "punycode": "^2.1.0" } }, + "tree-kill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", + "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==", + "dev": true + }, "trim-right": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", diff --git a/package.json b/package.json index 3e66441..7512818 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "main": "app.js", "scripts": { "start": "node ./app.js", - "debug": "cross-env NODE_ENV=development npx nodemon --inspect ./app.js", - "test": "cross-env NODE_ENV=test jest", + "dev": "npx concurrently \"npm run server\" \"npm run client\" ", + "client": "cd react && npm start", + "server": "npx cross-env NODE_ENV=development npx nodemon --inspect ./app.js", + "test": "npx cross-env NODE_ENV=test jest", "heroku-postbuild": "cd react && npm install && npm run build" }, "cacheDirectories": [ @@ -38,6 +40,7 @@ "passport-local-mongoose": "^5.0.1" }, "devDependencies": { + "concurrently": "^3.6.0", "cross-env": "^5.2.0", "eslint": "^5.1.0", "eslint-config-airbnb-base": "^13.0.0", diff --git a/react/src/components/App.css b/react/src/components/App.css index fee6ae3..b8091d5 100644 --- a/react/src/components/App.css +++ b/react/src/components/App.css @@ -53,7 +53,6 @@ } #inputs { - transition: 0.4s ease-in-out; box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1); display: flex; height: 2.5rem; diff --git a/react/src/components/App.js b/react/src/components/App.js index 8314eff..d2dc93a 100644 --- a/react/src/components/App.js +++ b/react/src/components/App.js @@ -6,7 +6,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'; import './Container.css'; import './App.css'; -import TodosContainer from '../containers/TodosContainer'; +import MainViewContainer from '../containers/MainViewContainer'; import LoginForm from './user/LoginForm'; import SignupForm from './user/SignupForm'; @@ -22,7 +22,7 @@ export default class App extends React.PureComponent {
- +
diff --git a/react/src/components/Filters.js b/react/src/components/Filters.js index 7aac73f..062068c 100644 --- a/react/src/components/Filters.js +++ b/react/src/components/Filters.js @@ -2,9 +2,9 @@ import React from 'react'; import FilterLink from '../containers/FilterLink'; import { VisibilityFilters } from '../actions/defs'; -function Filters() { +function Filters(styles) { return ( -
+
all active diff --git a/react/src/components/Input.js b/react/src/components/Input.js index 24e7cce..aca5493 100644 --- a/react/src/components/Input.js +++ b/react/src/components/Input.js @@ -3,18 +3,18 @@ import PropTypes from 'prop-types'; import { Button } from '@material-ui/core'; import AddIcon from '@material-ui/icons/Add'; -function Input(props) { +function Input({ onClick, styles }) { let input; function submit() { if (input.value.trim() !== '') { - props.onClick(input.value); + onClick(input.value); } input.value = ''; } return ( -
+
{ input = node; @@ -36,6 +36,7 @@ function Input(props) { } Input.propTypes = { + styles: PropTypes.any.isRequired, onClick: PropTypes.func.isRequired, }; diff --git a/react/src/components/MainView.js b/react/src/components/MainView.js new file mode 100644 index 0000000..cc4aa13 --- /dev/null +++ b/react/src/components/MainView.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import TodosContainer from '../containers/TodosContainer'; + +export default class MainView extends React.PureComponent { + componentDidUpdate() { + const { user, history } = this.props; + if (!user.user && !user.dirty) { + history.replace('/login'); + } + } + + render() { + return ; + } +} + +MainView.propTypes = { + user: PropTypes.any.isRequired, + history: PropTypes.any.isRequired, +}; diff --git a/react/src/components/Selector.js b/react/src/components/Selector.js index 5a9f621..3d725a4 100644 --- a/react/src/components/Selector.js +++ b/react/src/components/Selector.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Select, MenuItem } from '@material-ui/core'; import AddIcon from '@material-ui/icons/Add'; import CheckIcon from '@material-ui/icons/Check'; -import { Spring, animated } from 'react-spring'; +import { Transition, animated } from 'react-spring'; import './Selector.css'; @@ -28,65 +28,83 @@ export default function Selector({ if (creating) { let input = null; return ( -
- { - input = node; - }} - id="input" - type="text" - onKeyPress={e => { - if (e.key === 'Enter') { - addList(input.value); - } - }} - /> - - {styles => ( + + {styles => ( + + { + input = node; + }} + id="input" + type="text" + onKeyPress={e => { + if (e.key === 'Enter') { + addList(input.value); + } + }} + /> input.value.trim() && addList(input.value)} > - )} - -
+ + )} + ); } if (editing) { let input = null; return ( -
- { - input = node; - }} - defaultValue={lists.lists[list].name} - id="input" - type="text" - onKeyPress={e => { - if (e.key === 'Enter') { - editList(input.value); - } - }} - /> - - {styles => ( + + {styles => ( + + { + input = node; + }} + defaultValue={lists.lists[list].name} + id="input" + type="text" + onKeyPress={e => { + if (e.key === 'Enter') { + editList(input.value); + } + }} + /> input.value.trim() && editList(input.value)} > - )} - -
+ + )} + ); } if (list) { return ( -
+ -
+ ); } return null; diff --git a/react/src/components/Todos.js b/react/src/components/Todos.js index d454bab..39ee967 100644 --- a/react/src/components/Todos.js +++ b/react/src/components/Todos.js @@ -1,32 +1,35 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { Transition } from 'react-spring'; import InputContainer from '../containers/InputContainer'; import TodoListContainer from '../containers/TodoListContainer'; import Header from './Header'; import Filters from './Filters'; -export default class Todos extends React.PureComponent { - componentDidUpdate() { - const { user, history } = this.props; - if (!user.user && !user.dirty) { - history.replace('/login'); - } - } - - render() { - return ( -
-
- - - -
- ); - } +export default function Todos({ list }) { + return ( +
+
+ + {list && (styles => )} + + + + {list && Filters} + +
+ ); } Todos.propTypes = { - history: PropTypes.object.isRequired, - user: PropTypes.object.isRequired, + list: PropTypes.bool.isRequired, }; diff --git a/react/src/containers/InputContainer.js b/react/src/containers/InputContainer.js index da97978..d260ddc 100644 --- a/react/src/containers/InputContainer.js +++ b/react/src/containers/InputContainer.js @@ -3,6 +3,10 @@ import { connect } from 'react-redux'; import Input from '../components/Input'; import { addTodo } from '../actions/todos'; +function mapStateToProps(state, ownProps) { + return { ...ownProps }; +} + function mapDispatchToProps(dispatch) { return { onClick: text => dispatch(addTodo(text)), @@ -10,6 +14,6 @@ function mapDispatchToProps(dispatch) { } export default connect( - null, + mapStateToProps, mapDispatchToProps, )(Input); diff --git a/react/src/containers/MainViewContainer.js b/react/src/containers/MainViewContainer.js new file mode 100644 index 0000000..aca8084 --- /dev/null +++ b/react/src/containers/MainViewContainer.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; + +import MainView from '../components/MainView'; + +function mapStateToProps(state) { + return { + user: state.user, + }; +} + +export default withRouter(connect(mapStateToProps)(MainView)); diff --git a/react/src/containers/TodosContainer.js b/react/src/containers/TodosContainer.js index ab1563f..c083770 100644 --- a/react/src/containers/TodosContainer.js +++ b/react/src/containers/TodosContainer.js @@ -1,12 +1,11 @@ import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import Todos from '../components/Todos'; function mapStateToProps(state) { return { - user: state.user, + list: Boolean(state.lists.list), }; } -export default withRouter(connect(mapStateToProps)(Todos)); +export default connect(mapStateToProps)(Todos);