animate add todo/filter selector

This commit is contained in:
2018-07-13 22:38:13 +03:00
parent 5cfb9673b0
commit e3e1efa521
14 changed files with 284 additions and 114 deletions

40
app.js
View File

@@ -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 (

View File

@@ -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;
}
}

146
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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 {
<CssBaseline />
<Router>
<div id="container">
<Route exact path="/" component={TodosContainer} />
<Route exact path="/" component={MainViewContainer} />
<Route path="/login" component={LoginForm} />
<Route path="/signup" component={SignupForm} />
</div>

View File

@@ -2,9 +2,9 @@ import React from 'react';
import FilterLink from '../containers/FilterLink';
import { VisibilityFilters } from '../actions/defs';
function Filters() {
function Filters(styles) {
return (
<div id="filters">
<div style={styles} id="filters">
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>

View File

@@ -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 (
<div id="inputs">
<div style={styles} id="inputs">
<input
ref={node => {
input = node;
@@ -36,6 +36,7 @@ function Input(props) {
}
Input.propTypes = {
styles: PropTypes.any.isRequired,
onClick: PropTypes.func.isRequired,
};

View File

@@ -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 <TodosContainer />;
}
}
MainView.propTypes = {
user: PropTypes.any.isRequired,
history: PropTypes.any.isRequired,
};

View File

@@ -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 (
<div id="listselector" className="list--input">
<input
ref={node => {
input = node;
}}
id="input"
type="text"
onKeyPress={e => {
if (e.key === 'Enter') {
addList(input.value);
}
}}
/>
<Spring native from={{ opacity: 0 }} to={{ opacity: 1 }}>
{styles => (
<Transition
native
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 64 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{styles => (
<animated.div
style={styles}
id="listselector"
className="list--input"
>
<input
ref={node => {
input = node;
}}
id="input"
type="text"
onKeyPress={e => {
if (e.key === 'Enter') {
addList(input.value);
}
}}
/>
<animated.button
style={{ ...button, ...styles }}
onClick={() => input.value.trim() && addList(input.value)}
>
<AddIcon style={icon} />
</animated.button>
)}
</Spring>
</div>
</animated.div>
)}
</Transition>
);
}
if (editing) {
let input = null;
return (
<div id="listselector" className="list--input">
<input
ref={node => {
input = node;
}}
defaultValue={lists.lists[list].name}
id="input"
type="text"
onKeyPress={e => {
if (e.key === 'Enter') {
editList(input.value);
}
}}
/>
<Spring native from={{ opacity: 0 }} to={{ opacity: 1 }}>
{styles => (
<Transition
native
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 64 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{styles => (
<animated.div
style={styles}
id="listselector"
className="list--input"
>
<input
ref={node => {
input = node;
}}
defaultValue={lists.lists[list].name}
id="input"
type="text"
onKeyPress={e => {
if (e.key === 'Enter') {
editList(input.value);
}
}}
/>
<animated.button
style={{ ...button, ...styles }}
style={{ ...button }}
onClick={() => input.value.trim() && editList(input.value)}
>
<CheckIcon style={icon} />
</animated.button>
)}
</Spring>
</div>
</animated.div>
)}
</Transition>
);
}
if (list) {
return (
<div id="listselector">
<animated.div id="listselector">
<Select
style={{ fontSize: '1.5rem', width: '100%' }}
value={list}
@@ -98,7 +116,7 @@ export default function Selector({
</MenuItem>
))}
</Select>
</div>
</animated.div>
);
}
return null;

View File

@@ -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 (
<div id="todos">
<Header />
<InputContainer />
<TodoListContainer />
<Filters />
</div>
);
}
export default function Todos({ list }) {
return (
<div id="todos">
<Header />
<Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 38 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && (styles => <InputContainer styles={styles} />)}
</Transition>
<TodoListContainer />
<Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 32 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && Filters}
</Transition>
</div>
);
}
Todos.propTypes = {
history: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
list: PropTypes.bool.isRequired,
};

View File

@@ -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);

View File

@@ -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));

View File

@@ -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);