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

8
app.js
View File

@@ -3,12 +3,12 @@ const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const morgan = require('morgan'); const morgan = require('morgan');
const cors = require('cors'); const cors = require('cors');
const config = require('./config');
const db = require('./config/db');
const path = require('path'); const path = require('path');
const hsts = require('hsts'); const hsts = require('hsts');
const compression = require('compression'); const compression = require('compression');
const { redirectToHTTPS } = require('express-http-to-https'); const { redirectToHTTPS } = require('express-http-to-https');
const db = require('./config/db');
const config = require('./config');
require('./models/TodoList'); require('./models/TodoList');
require('./models/User'); require('./models/User');
@@ -74,6 +74,9 @@ app.use((req, res) => {
// handle errors // handle errors
app.use((error, req, res, next) => { app.use((error, req, res, next) => {
if (error.code) {
res.status(error.code);
} else {
switch (error.name) { switch (error.name) {
case 'ValidationError': case 'ValidationError':
case 'MissingPasswordError': case 'MissingPasswordError':
@@ -91,6 +94,7 @@ app.use((error, req, res, next) => {
default: default:
res.status(500); res.status(500);
} }
}
res.json({ success: false, error }); res.json({ success: false, error });
if ( if (
process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'production' ||

View File

@@ -4,6 +4,7 @@ class NotFoundError extends Error {
Error.captureStackTrace(this, NotFoundError); Error.captureStackTrace(this, NotFoundError);
this.name = 'NotFound'; this.name = 'NotFound';
this.text = text; this.text = text;
this.code = 404;
} }
} }
@@ -13,6 +14,7 @@ class BadRequestError extends Error {
Error.captureStackTrace(this, NotFoundError); Error.captureStackTrace(this, NotFoundError);
this.name = 'BadRequest'; this.name = 'BadRequest';
this.text = text; this.text = text;
this.code = 400;
} }
} }

146
package-lock.json generated
View File

@@ -1688,6 +1688,94 @@
"typedarray": "^0.0.6" "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": { "configstore": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz",
@@ -1853,6 +1941,12 @@
"whatwg-url": "^6.4.0" "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": { "debug": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -3514,14 +3608,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -3536,20 +3628,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -3666,8 +3755,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -3679,7 +3767,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -3694,7 +3781,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@@ -3702,14 +3788,12 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@@ -3728,7 +3812,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@@ -3809,8 +3892,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -3822,7 +3904,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -3944,7 +4025,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -5482,6 +5562,12 @@
"integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=",
"dev": true "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": { "json-schema": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@@ -7593,6 +7679,12 @@
"is-promise": "^2.1.0" "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": { "rx-lite": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
@@ -8001,6 +8093,12 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true "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": { "spdx-correct": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
@@ -8527,6 +8625,12 @@
"punycode": "^2.1.0" "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": { "trim-right": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",

View File

@@ -6,8 +6,10 @@
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "node ./app.js", "start": "node ./app.js",
"debug": "cross-env NODE_ENV=development npx nodemon --inspect ./app.js", "dev": "npx concurrently \"npm run server\" \"npm run client\" ",
"test": "cross-env NODE_ENV=test jest", "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" "heroku-postbuild": "cd react && npm install && npm run build"
}, },
"cacheDirectories": [ "cacheDirectories": [
@@ -38,6 +40,7 @@
"passport-local-mongoose": "^5.0.1" "passport-local-mongoose": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^3.6.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"eslint": "^5.1.0", "eslint": "^5.1.0",
"eslint-config-airbnb-base": "^13.0.0", "eslint-config-airbnb-base": "^13.0.0",

View File

@@ -53,7 +53,6 @@
} }
#inputs { #inputs {
transition: 0.4s ease-in-out;
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
display: flex; display: flex;
height: 2.5rem; height: 2.5rem;

View File

@@ -6,7 +6,7 @@ import CssBaseline from '@material-ui/core/CssBaseline';
import './Container.css'; import './Container.css';
import './App.css'; import './App.css';
import TodosContainer from '../containers/TodosContainer'; import MainViewContainer from '../containers/MainViewContainer';
import LoginForm from './user/LoginForm'; import LoginForm from './user/LoginForm';
import SignupForm from './user/SignupForm'; import SignupForm from './user/SignupForm';
@@ -22,7 +22,7 @@ export default class App extends React.PureComponent {
<CssBaseline /> <CssBaseline />
<Router> <Router>
<div id="container"> <div id="container">
<Route exact path="/" component={TodosContainer} /> <Route exact path="/" component={MainViewContainer} />
<Route path="/login" component={LoginForm} /> <Route path="/login" component={LoginForm} />
<Route path="/signup" component={SignupForm} /> <Route path="/signup" component={SignupForm} />
</div> </div>

View File

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

View File

@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
import { Button } from '@material-ui/core'; import { Button } from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add'; import AddIcon from '@material-ui/icons/Add';
function Input(props) { function Input({ onClick, styles }) {
let input; let input;
function submit() { function submit() {
if (input.value.trim() !== '') { if (input.value.trim() !== '') {
props.onClick(input.value); onClick(input.value);
} }
input.value = ''; input.value = '';
} }
return ( return (
<div id="inputs"> <div style={styles} id="inputs">
<input <input
ref={node => { ref={node => {
input = node; input = node;
@@ -36,6 +36,7 @@ function Input(props) {
} }
Input.propTypes = { Input.propTypes = {
styles: PropTypes.any.isRequired,
onClick: PropTypes.func.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 { Select, MenuItem } from '@material-ui/core';
import AddIcon from '@material-ui/icons/Add'; import AddIcon from '@material-ui/icons/Add';
import CheckIcon from '@material-ui/icons/Check'; import CheckIcon from '@material-ui/icons/Check';
import { Spring, animated } from 'react-spring'; import { Transition, animated } from 'react-spring';
import './Selector.css'; import './Selector.css';
@@ -28,7 +28,18 @@ export default function Selector({
if (creating) { if (creating) {
let input = null; let input = null;
return ( return (
<div id="listselector" className="list--input"> <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 <input
ref={node => { ref={node => {
input = node; input = node;
@@ -41,23 +52,32 @@ export default function Selector({
} }
}} }}
/> />
<Spring native from={{ opacity: 0 }} to={{ opacity: 1 }}>
{styles => (
<animated.button <animated.button
style={{ ...button, ...styles }} style={{ ...button, ...styles }}
onClick={() => input.value.trim() && addList(input.value)} onClick={() => input.value.trim() && addList(input.value)}
> >
<AddIcon style={icon} /> <AddIcon style={icon} />
</animated.button> </animated.button>
</animated.div>
)} )}
</Spring> </Transition>
</div>
); );
} }
if (editing) { if (editing) {
let input = null; let input = null;
return ( return (
<div id="listselector" className="list--input"> <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 <input
ref={node => { ref={node => {
input = node; input = node;
@@ -71,22 +91,20 @@ export default function Selector({
} }
}} }}
/> />
<Spring native from={{ opacity: 0 }} to={{ opacity: 1 }}>
{styles => (
<animated.button <animated.button
style={{ ...button, ...styles }} style={{ ...button }}
onClick={() => input.value.trim() && editList(input.value)} onClick={() => input.value.trim() && editList(input.value)}
> >
<CheckIcon style={icon} /> <CheckIcon style={icon} />
</animated.button> </animated.button>
</animated.div>
)} )}
</Spring> </Transition>
</div>
); );
} }
if (list) { if (list) {
return ( return (
<div id="listselector"> <animated.div id="listselector">
<Select <Select
style={{ fontSize: '1.5rem', width: '100%' }} style={{ fontSize: '1.5rem', width: '100%' }}
value={list} value={list}
@@ -98,7 +116,7 @@ export default function Selector({
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</div> </animated.div>
); );
} }
return null; return null;

View File

@@ -1,32 +1,35 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Transition } from 'react-spring';
import InputContainer from '../containers/InputContainer'; import InputContainer from '../containers/InputContainer';
import TodoListContainer from '../containers/TodoListContainer'; import TodoListContainer from '../containers/TodoListContainer';
import Header from './Header'; import Header from './Header';
import Filters from './Filters'; import Filters from './Filters';
export default class Todos extends React.PureComponent { export default function Todos({ list }) {
componentDidUpdate() {
const { user, history } = this.props;
if (!user.user && !user.dirty) {
history.replace('/login');
}
}
render() {
return ( return (
<div id="todos"> <div id="todos">
<Header /> <Header />
<InputContainer /> <Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 38 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && (styles => <InputContainer styles={styles} />)}
</Transition>
<TodoListContainer /> <TodoListContainer />
<Filters /> <Transition
from={{ opacity: 0, maxHeight: 0 }}
enter={{ opacity: 1, maxHeight: 32 }}
leave={{ opacity: 0, maxHeight: 0 }}
>
{list && Filters}
</Transition>
</div> </div>
); );
} }
}
Todos.propTypes = { Todos.propTypes = {
history: PropTypes.object.isRequired, list: PropTypes.bool.isRequired,
user: PropTypes.object.isRequired,
}; };

View File

@@ -3,6 +3,10 @@ import { connect } from 'react-redux';
import Input from '../components/Input'; import Input from '../components/Input';
import { addTodo } from '../actions/todos'; import { addTodo } from '../actions/todos';
function mapStateToProps(state, ownProps) {
return { ...ownProps };
}
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
onClick: text => dispatch(addTodo(text)), onClick: text => dispatch(addTodo(text)),
@@ -10,6 +14,6 @@ function mapDispatchToProps(dispatch) {
} }
export default connect( export default connect(
null, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
)(Input); )(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 { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import Todos from '../components/Todos'; import Todos from '../components/Todos';
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
user: state.user, list: Boolean(state.lists.list),
}; };
} }
export default withRouter(connect(mapStateToProps)(Todos)); export default connect(mapStateToProps)(Todos);