mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 15:47:48 +01:00
update, lint, prettify everything
put backend sources into src setup circleci don't use react-loadable because it seems unnecessary
This commit is contained in:
73
.circleci/config.yml
Normal file
73
.circleci/config.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
version: 2
|
||||
jobs:
|
||||
test-backend:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
|
||||
working_directory: ~/todolist
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: install backend deps
|
||||
command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: test backend
|
||||
command: npm test
|
||||
|
||||
test-frontend:
|
||||
docker:
|
||||
- image: circleci/node:14
|
||||
|
||||
working_directory: ~/todolist/client
|
||||
|
||||
steps:
|
||||
- checkout:
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: install backend deps
|
||||
command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: backend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- frontend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: install frontend deps
|
||||
command: cd client && npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- client/node_modules
|
||||
key: frontend-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run:
|
||||
name: test client
|
||||
command: cd client && npm test
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
test:
|
||||
jobs:
|
||||
- test-backend
|
||||
- test-frontend
|
||||
@@ -1,2 +1,5 @@
|
||||
client/build/*
|
||||
client/node_modules/*
|
||||
*.css
|
||||
*.scss
|
||||
**package-lock.json
|
||||
@@ -1,39 +1,11 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:node/recommended",
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowTernary": true
|
||||
}
|
||||
],
|
||||
"no-console": "warn",
|
||||
"no-underscore-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["_id"]
|
||||
}
|
||||
],
|
||||
"func-names": "off",
|
||||
"node/no-unsupported-features/es-syntax": [
|
||||
"error",
|
||||
{
|
||||
"version": ">=10.0.0",
|
||||
"ignores": []
|
||||
}
|
||||
],
|
||||
"node/no-unsupported-features/es-builtins": [
|
||||
"error",
|
||||
{
|
||||
"version": ">=10.0.0",
|
||||
"ignores": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"rules": {},
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
image: node:10
|
||||
|
||||
stages:
|
||||
- test-backend
|
||||
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
- client/node_modules
|
||||
|
||||
test-backend:
|
||||
stage: test-backend
|
||||
services:
|
||||
- mongo:4
|
||||
script:
|
||||
- npm i npm@latest -g
|
||||
- npm i
|
||||
- npm test
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 4,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"prettier.eslintIntegration": true,
|
||||
"eslint.workingDirectories": [
|
||||
".",
|
||||
"./client"
|
||||
],
|
||||
"search.exclude": {
|
||||
"**/package-lock.json": true
|
||||
},
|
||||
"editor.insertSpaces": true,
|
||||
"jest.pathToJest": "npm test --"
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
119
app.js
119
app.js
@@ -1,119 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const cors = require('cors');
|
||||
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');
|
||||
require('./models/Todo');
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(cors());
|
||||
app.use(compression());
|
||||
process.env.NODE_ENV === 'production'
|
||||
? app.use(morgan('combined'))
|
||||
: app.use(morgan('dev'));
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && process.env.HSTS === 'true') {
|
||||
app.use(redirectToHTTPS([/localhost:(\d{4})/]));
|
||||
app.use(
|
||||
hsts({
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const passport = require('./config/passport');
|
||||
|
||||
app.use(passport.initialize());
|
||||
|
||||
// Addresses, starting with /__, are not cached by service worker
|
||||
// https://github.com/facebook/create-react-app/issues/2237
|
||||
|
||||
app.use('/__/users', require('./routes/users'));
|
||||
|
||||
const auth = require('./routes/auth');
|
||||
|
||||
app.use('/__/lists', auth.required, require('./routes/lists'));
|
||||
app.use('/__/todos', auth.required, require('./routes/todos'));
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.NODE_ENV === 'development'
|
||||
) {
|
||||
app.use(express.static(path.join(__dirname, 'client/build')));
|
||||
app.use('*', express.static(path.join(__dirname, 'client/build/index.html')));
|
||||
}
|
||||
|
||||
// 404 route
|
||||
app.use((req, res) => {
|
||||
res.status(404);
|
||||
|
||||
if (req.accepts('html')) {
|
||||
res.send('404');
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.accepts('json')) {
|
||||
res.send({ error: 'Not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.type('txt').send('not found');
|
||||
});
|
||||
|
||||
// handle errors
|
||||
app.use((error, req, res, next) => {
|
||||
if (error.status) {
|
||||
res.status(error.status);
|
||||
} 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 (
|
||||
process.env.NODE_ENV === 'production' ||
|
||||
process.env.NODE_ENV === 'test'
|
||||
) {
|
||||
console.error(error);
|
||||
}
|
||||
next(error);
|
||||
});
|
||||
|
||||
let server;
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
db.connect();
|
||||
server = app.listen(config.app.port, () => {
|
||||
console.log(`Listening on port ${config.app.port}`);
|
||||
console.log('Started!');
|
||||
});
|
||||
} else {
|
||||
server = app;
|
||||
}
|
||||
|
||||
module.exports = server;
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = fn => (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
@@ -1,25 +1,22 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:jest/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [".js", ".jsx"]
|
||||
}
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:jest/recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"linebreak-style": "off",
|
||||
"react/forbid-prop-types": "off",
|
||||
"node/no-unsupported-features/es-syntax": ["off"],
|
||||
"node/no-unsupported-features/es-builtins": ["off"],
|
||||
"node/no-unsupported-features/node-builtins": ["off"],
|
||||
"react/display-name": ["warn"],
|
||||
"no-console": "warn"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
"rules": {
|
||||
"react/display-name": "warn"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
|
||||
11283
client/package-lock.json
generated
11283
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,28 +3,29 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@redux-offline/redux-offline": "^2.6.0",
|
||||
"http-proxy-middleware": "^1.0.6",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-scripts": "3.4.3",
|
||||
"react-spring": "^5.0.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-spring": "^8.0.27",
|
||||
"redux": "^4.0.5",
|
||||
"redux-form": "^8.3.6",
|
||||
"redux-form": "^8.3.7",
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
|
||||
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
export const ADD_LIST = 'ADD_LIST';
|
||||
export const REMOVE_LIST = 'REMOVE_LIST';
|
||||
export const EDIT_LIST_NAME = 'EDIT_LIST_NAME';
|
||||
export const RECIEVE_LISTS = 'RECIEVE_LISTS';
|
||||
export const REQUEST_LISTS = 'REQUEST_LISTS';
|
||||
export const INVALIDATE_LISTS = 'INVALIDATE_LISTS';
|
||||
export const VALIDATE_LISTS = 'VALIDATE_LISTS';
|
||||
export const CHANGE_LIST = 'CHANGE_LIST';
|
||||
export const START_CREATE_LIST = 'START_CREATE_LIST';
|
||||
export const START_EDIT_LIST = 'START_EDIT_LIST';
|
||||
export const STOP_CREATE_LIST = 'STOP_CREATE_LIST';
|
||||
export const STOP_EDIT_LIST = 'STOP_EDIT_LIST';
|
||||
export const ADD_LIST = "ADD_LIST";
|
||||
export const REMOVE_LIST = "REMOVE_LIST";
|
||||
export const EDIT_LIST_NAME = "EDIT_LIST_NAME";
|
||||
export const RECIEVE_LISTS = "RECIEVE_LISTS";
|
||||
export const REQUEST_LISTS = "REQUEST_LISTS";
|
||||
export const INVALIDATE_LISTS = "INVALIDATE_LISTS";
|
||||
export const VALIDATE_LISTS = "VALIDATE_LISTS";
|
||||
export const CHANGE_LIST = "CHANGE_LIST";
|
||||
export const START_CREATE_LIST = "START_CREATE_LIST";
|
||||
export const START_EDIT_LIST = "START_EDIT_LIST";
|
||||
export const STOP_CREATE_LIST = "STOP_CREATE_LIST";
|
||||
export const STOP_EDIT_LIST = "STOP_EDIT_LIST";
|
||||
|
||||
export const ADD_TODO = 'ADD_TODO';
|
||||
export const REMOVE_TODO = 'REMOVE_TODO';
|
||||
export const TOGGLE_TODO = 'TOGGLE_TODO';
|
||||
export const EDIT_TODO = 'EDIT_TODO';
|
||||
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
|
||||
export const RECIEVE_TODOS = 'RECIEVE_TODOS';
|
||||
export const REQUEST_TODOS = 'REQUEST_TODOS';
|
||||
export const INVALIDATE_TODOS = 'INVALIDATE_TODOS';
|
||||
export const VALIDATE_TODOS = 'VALIDATE_TODOS';
|
||||
export const ADD_TODO = "ADD_TODO";
|
||||
export const REMOVE_TODO = "REMOVE_TODO";
|
||||
export const TOGGLE_TODO = "TOGGLE_TODO";
|
||||
export const EDIT_TODO = "EDIT_TODO";
|
||||
export const SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
|
||||
export const RECIEVE_TODOS = "RECIEVE_TODOS";
|
||||
export const REQUEST_TODOS = "REQUEST_TODOS";
|
||||
export const INVALIDATE_TODOS = "INVALIDATE_TODOS";
|
||||
export const VALIDATE_TODOS = "VALIDATE_TODOS";
|
||||
|
||||
export const VisibilityFilters = {
|
||||
SHOW_ALL: 'SHOW_ALL',
|
||||
SHOW_COMPLETED: 'SHOW_COMPLETED',
|
||||
SHOW_ACTIVE: 'SHOW_ACTIVE',
|
||||
SHOW_ALL: "SHOW_ALL",
|
||||
SHOW_COMPLETED: "SHOW_COMPLETED",
|
||||
SHOW_ACTIVE: "SHOW_ACTIVE",
|
||||
};
|
||||
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAIL = 'LOGIN_FAIL';
|
||||
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS';
|
||||
export const SIGNUP_FAIL = 'SIGNUP_FAIL';
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
export const START_LOGIN = 'INVALIDATE_USER';
|
||||
export const REQUEST_USER = 'REQUEST_USER';
|
||||
export const VALIDATE_USER = 'VALIDATE_USER';
|
||||
export const RESET_USER = 'RESET_USER';
|
||||
export const EDIT_START = 'EDIT_START';
|
||||
export const EDIT_SUCCESS = 'EDIT_SUCCESS';
|
||||
export const EDIT_FAIL = 'EDIT_FAIL';
|
||||
export const RESET_EDIT = 'RESET_EDIT';
|
||||
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
|
||||
export const LOGIN_FAIL = "LOGIN_FAIL";
|
||||
export const SIGNUP_SUCCESS = "SIGNUP_SUCCESS";
|
||||
export const SIGNUP_FAIL = "SIGNUP_FAIL";
|
||||
export const LOGOUT = "LOGOUT";
|
||||
export const START_LOGIN = "INVALIDATE_USER";
|
||||
export const REQUEST_USER = "REQUEST_USER";
|
||||
export const VALIDATE_USER = "VALIDATE_USER";
|
||||
export const RESET_USER = "RESET_USER";
|
||||
export const EDIT_START = "EDIT_START";
|
||||
export const EDIT_SUCCESS = "EDIT_SUCCESS";
|
||||
export const EDIT_FAIL = "EDIT_FAIL";
|
||||
export const RESET_EDIT = "RESET_EDIT";
|
||||
|
||||
@@ -1,162 +1,162 @@
|
||||
import {
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
CHANGE_LIST,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
STOP_CREATE_LIST,
|
||||
STOP_EDIT_LIST,
|
||||
ADD_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
RECIEVE_TODOS,
|
||||
} from './defs';
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
CHANGE_LIST,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
STOP_CREATE_LIST,
|
||||
STOP_EDIT_LIST,
|
||||
ADD_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
RECIEVE_TODOS,
|
||||
} from "./defs";
|
||||
|
||||
import { API_ROOT, getToken, mongoObjectId } from './util';
|
||||
import { API_ROOT, getToken, mongoObjectId } from "./util";
|
||||
|
||||
function requestLists() {
|
||||
return { type: REQUEST_LISTS };
|
||||
return { type: REQUEST_LISTS };
|
||||
}
|
||||
function recieveLists(lists) {
|
||||
return { type: RECIEVE_LISTS, lists };
|
||||
return { type: RECIEVE_LISTS, lists };
|
||||
}
|
||||
export function changeList(list) {
|
||||
return { type: CHANGE_LIST, list };
|
||||
return { type: CHANGE_LIST, list };
|
||||
}
|
||||
export function startCreateList() {
|
||||
return { type: START_CREATE_LIST };
|
||||
return { type: START_CREATE_LIST };
|
||||
}
|
||||
export function startEditList() {
|
||||
return { type: START_EDIT_LIST };
|
||||
return { type: START_EDIT_LIST };
|
||||
}
|
||||
export function stopCreateList() {
|
||||
return { type: STOP_CREATE_LIST };
|
||||
return { type: STOP_CREATE_LIST };
|
||||
}
|
||||
export function stopEditList() {
|
||||
return { type: STOP_EDIT_LIST };
|
||||
return { type: STOP_EDIT_LIST };
|
||||
}
|
||||
|
||||
export function addList(name) {
|
||||
return async dispatch => {
|
||||
const id = mongoObjectId();
|
||||
dispatch({
|
||||
type: ADD_LIST,
|
||||
list: {
|
||||
name,
|
||||
id,
|
||||
todos: [],
|
||||
},
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists`,
|
||||
body: JSON.stringify({ name, id }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
return async (dispatch) => {
|
||||
const id = mongoObjectId();
|
||||
dispatch({
|
||||
type: ADD_LIST,
|
||||
list: {
|
||||
name,
|
||||
id,
|
||||
todos: [],
|
||||
},
|
||||
method: 'POST',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists`,
|
||||
body: JSON.stringify({ name, id }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeList() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch({
|
||||
type: REMOVE_LIST,
|
||||
list,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch({
|
||||
type: REMOVE_LIST,
|
||||
list,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function editList(name) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch({
|
||||
type: EDIT_LIST_NAME,
|
||||
list,
|
||||
name,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}`,
|
||||
body: JSON.stringify({ name }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch({
|
||||
type: EDIT_LIST_NAME,
|
||||
list,
|
||||
name,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}`,
|
||||
body: JSON.stringify({ name }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
method: 'PATCH',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTodos(lists) {
|
||||
return lists.reduce((todos, list) => {
|
||||
const listTodosObj = list.todos.reduce(
|
||||
(listTodos, todo) => ({
|
||||
...listTodos,
|
||||
[todo.id]: { ...todo },
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return { ...todos, ...listTodosObj };
|
||||
}, {});
|
||||
return lists.reduce((todos, list) => {
|
||||
const listTodosObj = list.todos.reduce(
|
||||
(listTodos, todo) => ({
|
||||
...listTodos,
|
||||
[todo.id]: { ...todo },
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return { ...todos, ...listTodosObj };
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function fetchLists() {
|
||||
return async dispatch => {
|
||||
dispatch(requestLists());
|
||||
const response = await fetch(`${API_ROOT}/lists`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const lists = json.data;
|
||||
const listsObj = lists.reduce((obj, curList) => {
|
||||
const newObj = { ...obj };
|
||||
newObj[curList.id] = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
editing: false,
|
||||
...curList,
|
||||
todos: curList.todos.map(todo => todo.id),
|
||||
};
|
||||
return newObj;
|
||||
}, {});
|
||||
return async (dispatch) => {
|
||||
dispatch(requestLists());
|
||||
const response = await fetch(`${API_ROOT}/lists`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const lists = json.data;
|
||||
const listsObj = lists.reduce((obj, curList) => {
|
||||
const newObj = { ...obj };
|
||||
newObj[curList.id] = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
editing: false,
|
||||
...curList,
|
||||
todos: curList.todos.map((todo) => todo.id),
|
||||
};
|
||||
return newObj;
|
||||
}, {});
|
||||
|
||||
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
|
||||
dispatch(recieveLists(listsObj));
|
||||
};
|
||||
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
|
||||
dispatch(recieveLists(listsObj));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,137 +1,137 @@
|
||||
import {
|
||||
REQUEST_TODOS,
|
||||
RECIEVE_TODOS,
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
EDIT_TODO,
|
||||
INVALIDATE_LISTS,
|
||||
} from './defs';
|
||||
REQUEST_TODOS,
|
||||
RECIEVE_TODOS,
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
EDIT_TODO,
|
||||
INVALIDATE_LISTS,
|
||||
} from "./defs";
|
||||
|
||||
import { API_ROOT, getToken, mongoObjectId } from './util';
|
||||
import { API_ROOT, getToken, mongoObjectId } from "./util";
|
||||
|
||||
export function fetchTodos() {
|
||||
return async dispatch => {
|
||||
dispatch({ type: REQUEST_TODOS });
|
||||
const response = await fetch(`${API_ROOT}/todos`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const todos = json.data;
|
||||
dispatch({ type: RECIEVE_TODOS, todos });
|
||||
};
|
||||
return async (dispatch) => {
|
||||
dispatch({ type: REQUEST_TODOS });
|
||||
const response = await fetch(`${API_ROOT}/todos`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const todos = json.data;
|
||||
dispatch({ type: RECIEVE_TODOS, todos });
|
||||
};
|
||||
}
|
||||
|
||||
export function addTodo(text) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
const id = mongoObjectId();
|
||||
if (list) {
|
||||
dispatch({
|
||||
type: ADD_TODO,
|
||||
todo: {
|
||||
text,
|
||||
id,
|
||||
completed: false,
|
||||
},
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}/todos`,
|
||||
body: JSON.stringify({ text, id }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
const id = mongoObjectId();
|
||||
if (list) {
|
||||
dispatch({
|
||||
type: ADD_TODO,
|
||||
todo: {
|
||||
text,
|
||||
id,
|
||||
completed: false,
|
||||
},
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}/todos`,
|
||||
body: JSON.stringify({ text, id }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTodo(id) {
|
||||
return async dispatch => {
|
||||
dispatch({
|
||||
type: REMOVE_TODO,
|
||||
id,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
return async (dispatch) => {
|
||||
dispatch({
|
||||
type: REMOVE_TODO,
|
||||
id,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleTodo(id) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const todoObj = state.todos.todos[id];
|
||||
const completed = !todoObj.completed;
|
||||
dispatch({
|
||||
type: TOGGLE_TODO,
|
||||
id,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
body: JSON.stringify({ completed }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const todoObj = state.todos.todos[id];
|
||||
const completed = !todoObj.completed;
|
||||
dispatch({
|
||||
type: TOGGLE_TODO,
|
||||
id,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
body: JSON.stringify({ completed }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
method: 'PATCH',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function editTodo(id, text) {
|
||||
return async dispatch => {
|
||||
dispatch({
|
||||
type: EDIT_TODO,
|
||||
id,
|
||||
text,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
body: JSON.stringify({ text }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
return async (dispatch) => {
|
||||
dispatch({
|
||||
type: EDIT_TODO,
|
||||
id,
|
||||
text,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
body: JSON.stringify({ text }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
method: 'PATCH',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,188 +1,186 @@
|
||||
import {
|
||||
START_LOGIN,
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
VALIDATE_USER,
|
||||
SIGNUP_SUCCESS,
|
||||
SIGNUP_FAIL,
|
||||
RESET_USER,
|
||||
LOGOUT,
|
||||
EDIT_START,
|
||||
EDIT_SUCCESS,
|
||||
EDIT_FAIL,
|
||||
RESET_EDIT,
|
||||
} from './defs';
|
||||
START_LOGIN,
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
VALIDATE_USER,
|
||||
SIGNUP_SUCCESS,
|
||||
SIGNUP_FAIL,
|
||||
RESET_USER,
|
||||
LOGOUT,
|
||||
EDIT_START,
|
||||
EDIT_SUCCESS,
|
||||
EDIT_FAIL,
|
||||
RESET_EDIT,
|
||||
} from "./defs";
|
||||
|
||||
import { API_ROOT, getToken, setToken } from './util';
|
||||
import { fetchLists } from './lists';
|
||||
import { API_ROOT, getToken, setToken } from "./util";
|
||||
import { fetchLists } from "./lists";
|
||||
|
||||
function startLogin() {
|
||||
return { type: START_LOGIN };
|
||||
return { type: START_LOGIN };
|
||||
}
|
||||
|
||||
function loginSuccess(user) {
|
||||
return { type: LOGIN_SUCCESS, user };
|
||||
return { type: LOGIN_SUCCESS, user };
|
||||
}
|
||||
|
||||
function loginFail(error) {
|
||||
return { type: LOGIN_FAIL, error };
|
||||
return { type: LOGIN_FAIL, error };
|
||||
}
|
||||
|
||||
function validateUser() {
|
||||
return { type: VALIDATE_USER };
|
||||
return { type: VALIDATE_USER };
|
||||
}
|
||||
|
||||
export function loadUser() {
|
||||
return async dispatch => {
|
||||
if (getToken()) {
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
} else {
|
||||
dispatch(validateUser());
|
||||
}
|
||||
};
|
||||
return async (dispatch) => {
|
||||
if (getToken()) {
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
} else {
|
||||
dispatch(validateUser());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function login(user) {
|
||||
return async dispatch => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users/login`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(json.data.jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
};
|
||||
return async (dispatch) => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users/login`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(json.data.jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loginJWT(jwt) {
|
||||
return async dispatch => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
};
|
||||
return async (dispatch) => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function signupSuccess(user) {
|
||||
return { type: SIGNUP_SUCCESS, user };
|
||||
return { type: SIGNUP_SUCCESS, user };
|
||||
}
|
||||
|
||||
function signupFail(error) {
|
||||
return { type: SIGNUP_FAIL, error };
|
||||
return { type: SIGNUP_FAIL, error };
|
||||
}
|
||||
|
||||
export function signup(user) {
|
||||
return async dispatch => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(json.data.jwt);
|
||||
dispatch(signupSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(signupFail(json.error));
|
||||
}
|
||||
};
|
||||
return async (dispatch) => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(json.data.jwt);
|
||||
dispatch(signupSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(signupFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function startEdit(user) {
|
||||
return { type: EDIT_START, user };
|
||||
return { type: EDIT_START, user };
|
||||
}
|
||||
|
||||
function editSuccess(user) {
|
||||
return { type: EDIT_SUCCESS, user };
|
||||
return { type: EDIT_SUCCESS, user };
|
||||
}
|
||||
|
||||
function editFail(error) {
|
||||
return { type: EDIT_FAIL, error };
|
||||
return { type: EDIT_FAIL, error };
|
||||
}
|
||||
|
||||
export function edit(user) {
|
||||
return async dispatch => {
|
||||
dispatch(startEdit());
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(editSuccess(json.data));
|
||||
} else {
|
||||
dispatch(editFail(json.error));
|
||||
}
|
||||
};
|
||||
return async (dispatch) => {
|
||||
dispatch(startEdit());
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(editSuccess(json.data));
|
||||
} else {
|
||||
dispatch(editFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteUser() {
|
||||
return async dispatch => {
|
||||
await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'DELETE',
|
||||
});
|
||||
dispatch(reset());
|
||||
};
|
||||
return async (dispatch) => {
|
||||
await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
});
|
||||
dispatch(reset());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function resetEdit() {
|
||||
return { type: RESET_EDIT };
|
||||
return { type: RESET_EDIT };
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
return { type: RESET_USER };
|
||||
return { type: RESET_USER };
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return async dispatch => {
|
||||
dispatch({ type: LOGOUT });
|
||||
};
|
||||
return async (dispatch) => {
|
||||
dispatch({ type: LOGOUT });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
export const API_ROOT = '/__';
|
||||
export const API_ROOT = "/__";
|
||||
|
||||
let token = null;
|
||||
|
||||
export function setToken(_token) {
|
||||
token = _token;
|
||||
token = _token;
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return token;
|
||||
return token;
|
||||
}
|
||||
|
||||
export function mongoObjectId() {
|
||||
// eslint-disable-next-line
|
||||
const timestamp = ((new Date().getTime() / 1000) | 0).toString(16);
|
||||
return (
|
||||
timestamp +
|
||||
'xxxxxxxxxxxxxxxx'
|
||||
// eslint-disable-next-line
|
||||
.replace(/[x]/g, () => ((Math.random() * 16) | 0).toString(16))
|
||||
.toLowerCase()
|
||||
);
|
||||
// eslint-disable-next-line
|
||||
const timestamp = ((new Date().getTime() / 1000) | 0).toString(16);
|
||||
return (
|
||||
timestamp +
|
||||
"xxxxxxxxxxxxxxxx"
|
||||
// eslint-disable-next-line
|
||||
.replace(/[x]/g, () => ((Math.random() * 16) | 0).toString(16))
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SET_VISIBILITY_FILTER } from './defs';
|
||||
import { SET_VISIBILITY_FILTER } from "./defs";
|
||||
|
||||
export default function setVisibilityFilter(filter) {
|
||||
return { type: SET_VISIBILITY_FILTER, filter };
|
||||
return { type: SET_VISIBILITY_FILTER, filter };
|
||||
}
|
||||
|
||||
@@ -1,246 +1,246 @@
|
||||
#lists-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #fbfbfb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
#lists {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 80%;
|
||||
margin-right: 1rem;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 80%;
|
||||
margin-right: 1rem;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
align-self: center;
|
||||
margin-left: 2rem;
|
||||
align-self: center;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
#listactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0.2rem 0.1rem;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0.2rem 0.1rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
#listactions button {
|
||||
color: #555555;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
color: #555555;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
}
|
||||
|
||||
#listactions .backbutton {
|
||||
margin: 1rem 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#filters button {
|
||||
min-width: 3rem;
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
#inputs {
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#input {
|
||||
color: black;
|
||||
background: white;
|
||||
font-family: 'Roboto';
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: 1rem;
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
border: none;
|
||||
color: black;
|
||||
background: white;
|
||||
font-family: "Roboto";
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: 1rem;
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#input::placeholder {
|
||||
opacity: 0.35;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
#input:focus::placeholder {
|
||||
opacity: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#add {
|
||||
color: black;
|
||||
flex-grow: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
max-width: 2rem;
|
||||
background-color: white;
|
||||
border: none;
|
||||
color: black;
|
||||
flex-grow: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
max-width: 2rem;
|
||||
background-color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
li:first-child .delete,
|
||||
li:first-child .edit,
|
||||
li:first-child .save,
|
||||
li:first-child .todo {
|
||||
border-top: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.done {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
text-decoration: line-through;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
li button {
|
||||
color: black;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: none;
|
||||
width: 2rem;
|
||||
font-size: 1rem;
|
||||
transition: 0.17s ease-in-out;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 3px 0 6px -3px rgba(0, 0, 0, 0.3);
|
||||
color: black;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: none;
|
||||
width: 2rem;
|
||||
font-size: 1rem;
|
||||
transition: 0.17s ease-in-out;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 3px 0 6px -3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* make it usable on smartphones */
|
||||
@media only screen and (max-width: 600px) {
|
||||
#header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
}
|
||||
#inputs {
|
||||
position: fixed;
|
||||
top: 8rem;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
max-height: 65px !important;
|
||||
}
|
||||
#container {
|
||||
margin-top: 12rem;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
#filters {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-height: 3rem !important;
|
||||
opacity: 1 !important;
|
||||
background-color: white;
|
||||
}
|
||||
#filters button {
|
||||
height: 3rem !important;
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
li button {
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
#header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
}
|
||||
#inputs {
|
||||
position: fixed;
|
||||
top: 8rem;
|
||||
left: 0;
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 65px;
|
||||
max-height: 65px !important;
|
||||
}
|
||||
#container {
|
||||
margin-top: 12rem;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
#filters {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
max-height: 3rem !important;
|
||||
opacity: 1 !important;
|
||||
background-color: white;
|
||||
}
|
||||
#filters button {
|
||||
height: 3rem !important;
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
li button {
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
li button.todo {
|
||||
padding: 0.5rem;
|
||||
box-shadow: none;
|
||||
padding: 0.5rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.todo {
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-weight: 400;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 1;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-family: Roboto;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-weight: 400;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 1;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
textarea.todo--input {
|
||||
background: #fafafa;
|
||||
color: black;
|
||||
resize: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
font-family: 'Roboto';
|
||||
font-size: 1rem;
|
||||
transition: 0.1s ease-in-out;
|
||||
background: #fafafa;
|
||||
color: black;
|
||||
resize: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
font-family: "Roboto";
|
||||
font-size: 1rem;
|
||||
transition: 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
width: 2rem;
|
||||
background-color: #fafafa;
|
||||
color: #555555;
|
||||
box-shadow: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
width: 2rem;
|
||||
background-color: #fafafa;
|
||||
color: #555555;
|
||||
box-shadow: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
margin: 0.1rem;
|
||||
color: #555555;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: Roboto;
|
||||
margin: 0.1rem;
|
||||
color: #555555;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
.filter--active {
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -1,83 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import Loadable from 'react-loadable';
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { BrowserRouter as Router, Route } from "react-router-dom";
|
||||
import CssBaseline from "@material-ui/core/CssBaseline";
|
||||
|
||||
import Protected from './Protected';
|
||||
import OnlyUnauth from './OnlyUnauth';
|
||||
import './Container.css';
|
||||
import './App.css';
|
||||
import Protected from "./Protected";
|
||||
import OnlyUnauth from "./OnlyUnauth";
|
||||
import "./Container.css";
|
||||
import "./App.css";
|
||||
import TodosView from "./todolist/TodosView";
|
||||
import LoginForm from "./user/LoginForm";
|
||||
import SignupForm from "./user/SignupForm";
|
||||
import EditForm from "./user/EditForm";
|
||||
|
||||
function Loading(props) {
|
||||
if (props.error) {
|
||||
return (
|
||||
<div>
|
||||
Error! <button onClick={props.retry}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
} else if (props.pastDelay) {
|
||||
return <div>Loading...</div>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const ProtectedTodosView = Protected(TodosView);
|
||||
|
||||
const LoadableTodosView = Protected(
|
||||
Loadable({
|
||||
loader: () => import('./todolist/TodosView'),
|
||||
loading: () => Loading,
|
||||
delay: 1000,
|
||||
}),
|
||||
);
|
||||
const ProtectedLoginForm = OnlyUnauth(LoginForm);
|
||||
|
||||
const LoadableLoginForm = OnlyUnauth(
|
||||
Loadable({
|
||||
loader: () => import('./user/LoginForm'),
|
||||
loading: () => Loading,
|
||||
delay: 1000,
|
||||
}),
|
||||
);
|
||||
const ProtectedSignupForm = OnlyUnauth(SignupForm);
|
||||
|
||||
const LoadableSignupForm = OnlyUnauth(
|
||||
Loadable({
|
||||
loader: () => import('./user/SignupForm'),
|
||||
loading: () => Loading,
|
||||
delay: 1000,
|
||||
}),
|
||||
);
|
||||
|
||||
const LoadableEditView = Protected(
|
||||
Loadable({
|
||||
loader: () => import('./user/EditForm'),
|
||||
loading: () => Loading,
|
||||
delay: 1000,
|
||||
}),
|
||||
);
|
||||
const ProtectedEditView = Protected(EditForm);
|
||||
|
||||
export default class App extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { loadUser } = this.props;
|
||||
loadUser();
|
||||
}
|
||||
componentDidMount() {
|
||||
const { loadUser } = this.props;
|
||||
loadUser();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
<div id="container">
|
||||
<Route exact path="/" component={LoadableTodosView} />
|
||||
<Route path="/login" component={LoadableLoginForm} />
|
||||
<Route path="/signup" component={LoadableSignupForm} />
|
||||
<Route path="/edit" component={LoadableEditView} />
|
||||
</div>
|
||||
</Router>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
<div id="container">
|
||||
<Route exact path="/" component={ProtectedTodosView} />
|
||||
<Route path="/login" component={ProtectedLoginForm} />
|
||||
<Route path="/signup" component={ProtectedSignupForm} />
|
||||
<Route path="/edit" component={ProtectedEditView} />
|
||||
</div>
|
||||
</Router>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
loadUser: PropTypes.func.isRequired,
|
||||
loadUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import App from './App';
|
||||
import App from "./App";
|
||||
|
||||
import { loadUser } from '../actions/user';
|
||||
import { loadUser } from "../actions/user";
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
loadUser: () => dispatch(loadUser()),
|
||||
};
|
||||
return {
|
||||
loadUser: () => dispatch(loadUser()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(App);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons');
|
||||
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons");
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: 'Roboto';
|
||||
user-select: none;
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: "Roboto";
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
margin-top: 5rem;
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
#container {
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
max-width: 25rem;
|
||||
border: 1px solid #dddddd;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
max-width: 25rem;
|
||||
border: 1px solid #dddddd;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
#root {
|
||||
margin: 0;
|
||||
}
|
||||
#container {
|
||||
max-width: 100%;
|
||||
}
|
||||
#root {
|
||||
margin: 0;
|
||||
}
|
||||
#container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#user-header {
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
justify-content: flex-end;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
justify-content: flex-end;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
#user-header button {
|
||||
box-sizing: border-box;
|
||||
margin-right: 0.2rem;
|
||||
height: 100%;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
color: #555555;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 300;
|
||||
font-family: Roboto;
|
||||
box-sizing: border-box;
|
||||
margin-right: 0.2rem;
|
||||
height: 100%;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
color: #555555;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 300;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
#user-header button:hover {
|
||||
color: #555555;
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import UserHeader from './user/UserHeader';
|
||||
import Lists from './lists/Lists';
|
||||
import React from "react";
|
||||
import UserHeader from "./user/UserHeader";
|
||||
import Lists from "./lists/Lists";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div id="header">
|
||||
<UserHeader />
|
||||
<Lists />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div id="header">
|
||||
<UserHeader />
|
||||
<Lists />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
export default function OnlyUnauth(WrappedComponent) {
|
||||
function Component({ loggedIn }) {
|
||||
return loggedIn ? <Redirect to="/" /> : <WrappedComponent />;
|
||||
}
|
||||
function Component({ loggedIn }) {
|
||||
return loggedIn ? <Redirect to="/" /> : <WrappedComponent />;
|
||||
}
|
||||
|
||||
Component.propTypes = {
|
||||
loggedIn: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
loggedIn: state.user.user !== undefined && state.user.user !== null,
|
||||
Component.propTypes = {
|
||||
loggedIn: PropTypes.bool.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
)(Component);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
loggedIn: state.user.user !== undefined && state.user.user !== null,
|
||||
};
|
||||
}
|
||||
|
||||
return connect(mapStateToProps, null)(Component);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
export default function Protected(WrappedComponent) {
|
||||
function Component({ loggedIn }) {
|
||||
return loggedIn ? <WrappedComponent /> : <Redirect to="/login" />;
|
||||
}
|
||||
function Component({ loggedIn }) {
|
||||
return loggedIn ? <WrappedComponent /> : <Redirect to="/login" />;
|
||||
}
|
||||
|
||||
Component.propTypes = {
|
||||
loggedIn: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
loggedIn: state.user.user !== undefined && state.user.user !== null,
|
||||
Component.propTypes = {
|
||||
loggedIn: PropTypes.bool.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
)(Component);
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
loggedIn: state.user.user !== undefined && state.user.user !== null,
|
||||
};
|
||||
}
|
||||
|
||||
return connect(mapStateToProps, null)(Component);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,48 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import setVisibilityFilter from '../../actions/visibilityFilter';
|
||||
import { connect } from "react-redux";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { ButtonBase } from "@material-ui/core";
|
||||
import setVisibilityFilter from "../../actions/visibilityFilter";
|
||||
|
||||
function Link({ active, onClick, children }) {
|
||||
const classes = ['filter'];
|
||||
if (active) {
|
||||
classes.push('filter--active');
|
||||
}
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
padding: '0 1rem',
|
||||
color: active ? 'black' : '#444444',
|
||||
height: '2rem',
|
||||
}}
|
||||
className={classes.join(' ')}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
const classes = ["filter"];
|
||||
if (active) {
|
||||
classes.push("filter--active");
|
||||
}
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
padding: "0 1rem",
|
||||
color: active ? "black" : "#444444",
|
||||
height: "2rem",
|
||||
}}
|
||||
className={classes.join(" ")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
active: ownProps.filter === state.visibilityFilter,
|
||||
};
|
||||
return {
|
||||
active: ownProps.filter === state.visibilityFilter,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch, ownProps) {
|
||||
return {
|
||||
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
|
||||
};
|
||||
return {
|
||||
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Link);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Link);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react';
|
||||
import FilterLink from './FilterLink';
|
||||
import { VisibilityFilters } from '../../actions/defs';
|
||||
import React from "react";
|
||||
import FilterLink from "./FilterLink";
|
||||
import { VisibilityFilters } from "../../actions/defs";
|
||||
|
||||
function Filters(styles) {
|
||||
return (
|
||||
<div style={styles} id="filters">
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
|
||||
completed
|
||||
</FilterLink>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={styles} id="filters">
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
|
||||
active
|
||||
</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
|
||||
completed
|
||||
</FilterLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filters;
|
||||
|
||||
@@ -1,117 +1,117 @@
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import BackButton from '@material-ui/icons/ArrowBack';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition, config } from 'react-spring';
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import BackButton from "@material-ui/icons/ArrowBack";
|
||||
import { IconButton } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Transition, config } from "react-spring";
|
||||
|
||||
const button = {
|
||||
width: 30,
|
||||
height: 30,
|
||||
padding: 0,
|
||||
width: 30,
|
||||
height: 30,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const icon = {
|
||||
fontSize: 24,
|
||||
fontSize: 24,
|
||||
};
|
||||
|
||||
export default function ListActions({
|
||||
startCreateList,
|
||||
removeList,
|
||||
startEditList,
|
||||
stopCreateList,
|
||||
stopEditList,
|
||||
creating,
|
||||
editing,
|
||||
list,
|
||||
startCreateList,
|
||||
removeList,
|
||||
startEditList,
|
||||
stopCreateList,
|
||||
stopEditList,
|
||||
creating,
|
||||
editing,
|
||||
list,
|
||||
}) {
|
||||
function back() {
|
||||
if (editing) {
|
||||
stopEditList();
|
||||
function back() {
|
||||
if (editing) {
|
||||
stopEditList();
|
||||
}
|
||||
if (creating) {
|
||||
stopCreateList();
|
||||
}
|
||||
}
|
||||
if (creating) {
|
||||
stopCreateList();
|
||||
const actions = [];
|
||||
if (!creating && !editing) {
|
||||
actions.push((styles) => (
|
||||
<IconButton
|
||||
key="create"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => startCreateList()}
|
||||
>
|
||||
<AddIcon style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
}
|
||||
const actions = [];
|
||||
if (!creating && !editing) {
|
||||
actions.push(styles => (
|
||||
<IconButton
|
||||
key="create"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => startCreateList()}
|
||||
>
|
||||
<AddIcon style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
if (list && !creating && !editing) {
|
||||
actions.push(styles => (
|
||||
<IconButton
|
||||
key="remove"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => removeList()}
|
||||
>
|
||||
<DeleteIcon style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
if (list && !creating && !editing) {
|
||||
actions.push(styles => (
|
||||
<IconButton
|
||||
key="edit"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => startEditList()}
|
||||
>
|
||||
<EditIcon style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
if (creating || editing) {
|
||||
actions.push(styles => (
|
||||
<IconButton
|
||||
key="back"
|
||||
style={{ ...button, ...styles }}
|
||||
className="backbutton"
|
||||
onClick={() => back()}
|
||||
>
|
||||
<BackButton style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<div id="listactions">
|
||||
<Transition
|
||||
config={{
|
||||
...config.stiff,
|
||||
overshootClamping: true,
|
||||
restSpeedThreshold: 0.5,
|
||||
restDisplacementThreshold: 0.5,
|
||||
}}
|
||||
keys={actions.map(action => action({}).key)}
|
||||
from={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
|
||||
enter={{ opacity: 1, height: 30, margin: 0, padding: 0 }}
|
||||
leave={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
|
||||
>
|
||||
{actions}
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
if (list && !creating && !editing) {
|
||||
actions.push((styles) => (
|
||||
<IconButton
|
||||
key="remove"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => removeList()}
|
||||
>
|
||||
<DeleteIcon style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
if (list && !creating && !editing) {
|
||||
actions.push((styles) => (
|
||||
<IconButton
|
||||
key="edit"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => startEditList()}
|
||||
>
|
||||
<EditIcon style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
if (creating || editing) {
|
||||
actions.push((styles) => (
|
||||
<IconButton
|
||||
key="back"
|
||||
style={{ ...button, ...styles }}
|
||||
className="backbutton"
|
||||
onClick={() => back()}
|
||||
>
|
||||
<BackButton style={icon} />
|
||||
</IconButton>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<div id="listactions">
|
||||
<Transition
|
||||
config={{
|
||||
...config.stiff,
|
||||
overshootClamping: true,
|
||||
restSpeedThreshold: 0.5,
|
||||
restDisplacementThreshold: 0.5,
|
||||
}}
|
||||
keys={actions.map((action) => action({}).key)}
|
||||
from={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
|
||||
enter={{ opacity: 1, height: 30, margin: 0, padding: 0 }}
|
||||
leave={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
|
||||
>
|
||||
{actions}
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListActions.defaultProps = {
|
||||
list: '',
|
||||
list: "",
|
||||
};
|
||||
|
||||
ListActions.propTypes = {
|
||||
startCreateList: PropTypes.func.isRequired,
|
||||
removeList: PropTypes.func.isRequired,
|
||||
startEditList: PropTypes.func.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
list: PropTypes.string,
|
||||
stopCreateList: PropTypes.func.isRequired,
|
||||
stopEditList: PropTypes.func.isRequired,
|
||||
startCreateList: PropTypes.func.isRequired,
|
||||
removeList: PropTypes.func.isRequired,
|
||||
startEditList: PropTypes.func.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
list: PropTypes.string,
|
||||
stopCreateList: PropTypes.func.isRequired,
|
||||
stopEditList: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ListActions from './ListActions';
|
||||
import { connect } from "react-redux";
|
||||
import ListActions from "./ListActions";
|
||||
import {
|
||||
startCreateList,
|
||||
startEditList,
|
||||
removeList,
|
||||
stopCreateList,
|
||||
stopEditList,
|
||||
} from '../../actions/lists';
|
||||
startCreateList,
|
||||
startEditList,
|
||||
removeList,
|
||||
stopCreateList,
|
||||
stopEditList,
|
||||
} from "../../actions/lists";
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
list: state.lists.list,
|
||||
creating: state.lists.creating,
|
||||
editing: state.lists.editing,
|
||||
};
|
||||
return {
|
||||
list: state.lists.list,
|
||||
creating: state.lists.creating,
|
||||
editing: state.lists.editing,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
startCreateList: () => dispatch(startCreateList()),
|
||||
startEditList: () => dispatch(startEditList()),
|
||||
stopCreateList: () => dispatch(stopCreateList()),
|
||||
stopEditList: () => dispatch(stopEditList()),
|
||||
removeList: () => dispatch(removeList()),
|
||||
};
|
||||
return {
|
||||
startCreateList: () => dispatch(startCreateList()),
|
||||
startEditList: () => dispatch(startEditList()),
|
||||
stopCreateList: () => dispatch(stopCreateList()),
|
||||
stopEditList: () => dispatch(stopEditList()),
|
||||
removeList: () => dispatch(removeList()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ListActions);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ListActions);
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { IconButton } from "@material-ui/core";
|
||||
|
||||
const button = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 36,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
export default function ListInput({ onClick, children, defaultValue }) {
|
||||
let input;
|
||||
return (
|
||||
<div id="listselector" className="list--input">
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
style={{ height: 40 }}
|
||||
id="input"
|
||||
type="text"
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
onClick(input.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={button}
|
||||
type="submit"
|
||||
onClick={() => input.value.trim() && onClick(input.value)}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
let input;
|
||||
return (
|
||||
<div id="listselector" className="list--input">
|
||||
<input
|
||||
ref={(node) => {
|
||||
input = node;
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
style={{ height: 40 }}
|
||||
id="input"
|
||||
type="text"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
onClick(input.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={button}
|
||||
type="submit"
|
||||
onClick={() => input.value.trim() && onClick(input.value)}
|
||||
>
|
||||
{children}
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListInput.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
defaultValue: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
defaultValue: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from "react-redux";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import ListActionsContainer from './ListActionsContainer';
|
||||
import SelectorContainer from './SelectorContainer';
|
||||
import ListActionsContainer from "./ListActionsContainer";
|
||||
import SelectorContainer from "./SelectorContainer";
|
||||
|
||||
function Lists({ userLoaded, listsLoaded }) {
|
||||
return (
|
||||
<div id="lists-header">
|
||||
{userLoaded &&
|
||||
listsLoaded && (
|
||||
<div id="lists">
|
||||
<ListActionsContainer />
|
||||
<SelectorContainer />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div id="lists-header">
|
||||
{userLoaded && listsLoaded && (
|
||||
<div id="lists">
|
||||
<ListActionsContainer />
|
||||
<SelectorContainer />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Lists.propTypes = {
|
||||
userLoaded: PropTypes.bool.isRequired,
|
||||
listsLoaded: PropTypes.bool.isRequired,
|
||||
userLoaded: PropTypes.bool.isRequired,
|
||||
listsLoaded: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
userLoaded: state.user.loaded,
|
||||
listsLoaded: state.lists.loaded,
|
||||
};
|
||||
return {
|
||||
userLoaded: state.user.loaded,
|
||||
listsLoaded: state.lists.loaded,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Lists);
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
#listselector {
|
||||
display: flex;
|
||||
margin-left: 1rem;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
background-color: #fbfbfb;
|
||||
flex-grow: 1;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
margin-left: 1rem;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
background-color: #fbfbfb;
|
||||
flex-grow: 1;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#listselector input {
|
||||
padding: 0;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
border-bottom: 1px solid #888888;
|
||||
width: 80%;
|
||||
padding: 0;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
border-bottom: 1px solid #888888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#listselector button {
|
||||
align-self: center;
|
||||
width: 20%;
|
||||
font-size: 0.9rem;
|
||||
color: #1b881b;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
align-self: center;
|
||||
width: 20%;
|
||||
font-size: 0.9rem;
|
||||
color: #1b881b;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
}
|
||||
|
||||
#listselector select {
|
||||
max-width: 100%;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
max-width: 100%;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
#listselector select option {
|
||||
max-width: 100%;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
max-width: 100%;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
import React from 'react';
|
||||
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 React from "react";
|
||||
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 ListInput from './ListInput';
|
||||
import './Selector.css';
|
||||
import ListInput from "./ListInput";
|
||||
import "./Selector.css";
|
||||
|
||||
const icon = {
|
||||
fontSize: 24,
|
||||
fontSize: 24,
|
||||
};
|
||||
|
||||
export default function Selector({
|
||||
lists,
|
||||
list,
|
||||
onChange,
|
||||
editing,
|
||||
creating,
|
||||
addList,
|
||||
editList,
|
||||
lists,
|
||||
list,
|
||||
onChange,
|
||||
editing,
|
||||
creating,
|
||||
addList,
|
||||
editList,
|
||||
}) {
|
||||
if (creating) {
|
||||
return (
|
||||
<ListInput onClick={addList}>
|
||||
<AddIcon style={icon} />
|
||||
</ListInput>
|
||||
);
|
||||
}
|
||||
if (editing) {
|
||||
return (
|
||||
<ListInput onClick={editList} defaultValue={lists.lists[list].name}>
|
||||
<CheckIcon style={icon} />
|
||||
</ListInput>
|
||||
);
|
||||
}
|
||||
if (list) {
|
||||
return (
|
||||
<div id="listselector">
|
||||
<Select
|
||||
style={{ fontSize: '1.5rem', width: '100%', height: 40 }}
|
||||
value={list}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
{Object.values(lists.lists).map(elem => (
|
||||
<MenuItem key={elem.id} value={elem.id}>
|
||||
{elem.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
if (creating) {
|
||||
return (
|
||||
<ListInput onClick={addList}>
|
||||
<AddIcon style={icon} />
|
||||
</ListInput>
|
||||
);
|
||||
}
|
||||
if (editing) {
|
||||
return (
|
||||
<ListInput onClick={editList} defaultValue={lists.lists[list].name}>
|
||||
<CheckIcon style={icon} />
|
||||
</ListInput>
|
||||
);
|
||||
}
|
||||
if (list) {
|
||||
return (
|
||||
<div id="listselector">
|
||||
<Select
|
||||
style={{ fontSize: "1.5rem", width: "100%", height: 40 }}
|
||||
value={list}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
{Object.values(lists.lists).map((elem) => (
|
||||
<MenuItem key={elem.id} value={elem.id}>
|
||||
{elem.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Selector.defaultProps = {
|
||||
list: '',
|
||||
list: "",
|
||||
};
|
||||
|
||||
Selector.propTypes = {
|
||||
list: PropTypes.string,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
editList: PropTypes.func.isRequired,
|
||||
addList: PropTypes.func.isRequired,
|
||||
lists: PropTypes.object.isRequired,
|
||||
list: PropTypes.string,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
editList: PropTypes.func.isRequired,
|
||||
addList: PropTypes.func.isRequired,
|
||||
lists: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Selector from './Selector';
|
||||
import { changeList, addList, editList } from '../../actions/lists';
|
||||
import { connect } from "react-redux";
|
||||
import Selector from "./Selector";
|
||||
import { changeList, addList, editList } from "../../actions/lists";
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
lists: state.lists,
|
||||
list: state.lists.list,
|
||||
editing: state.lists.editing,
|
||||
creating: state.lists.creating,
|
||||
};
|
||||
return {
|
||||
lists: state.lists,
|
||||
list: state.lists.list,
|
||||
editing: state.lists.editing,
|
||||
creating: state.lists.creating,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onChange: list => dispatch(changeList(list)),
|
||||
addList: name => dispatch(addList(name)),
|
||||
editList: name => dispatch(editList(name)),
|
||||
};
|
||||
return {
|
||||
onChange: (list) => dispatch(changeList(list)),
|
||||
addList: (name) => dispatch(addList(name)),
|
||||
editList: (name) => dispatch(editList(name)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Selector);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Selector);
|
||||
|
||||
@@ -1,162 +1,168 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { animated } from 'react-spring';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { animated } from "react-spring";
|
||||
import { ButtonBase } from "@material-ui/core";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import CheckIcon from "@material-ui/icons/Check";
|
||||
|
||||
const icon = {
|
||||
fontSize: 24,
|
||||
padding: 0,
|
||||
fontSize: 24,
|
||||
padding: 0,
|
||||
};
|
||||
const disabledAction = {
|
||||
backgroundColor: '#fafafa',
|
||||
color: '#dddddd',
|
||||
backgroundColor: "#fafafa",
|
||||
color: "#dddddd",
|
||||
};
|
||||
|
||||
class Todo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
this.onMouseOver = this.onMouseOver.bind(this);
|
||||
this.onMouseOut = this.onMouseOut.bind(this);
|
||||
this.startEdit = this.startEdit.bind(this);
|
||||
this.stopEdit = this.stopEdit.bind(this);
|
||||
}
|
||||
|
||||
onMouseOver() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
hover: true,
|
||||
});
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
|
||||
stopEdit(value) {
|
||||
const { editTodo } = this.props;
|
||||
editTodo(value);
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
editing: false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteClasses = ['delete'];
|
||||
const editClasses = ['edit'];
|
||||
const { hover, editing } = this.state;
|
||||
const { todo, removeTodo, toggleTodo, style } = this.props;
|
||||
if (!hover) {
|
||||
deleteClasses.push('disabled');
|
||||
editClasses.push('disabled');
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
this.onMouseOver = this.onMouseOver.bind(this);
|
||||
this.onMouseOut = this.onMouseOut.bind(this);
|
||||
this.startEdit = this.startEdit.bind(this);
|
||||
this.stopEdit = this.stopEdit.bind(this);
|
||||
}
|
||||
|
||||
let input;
|
||||
onMouseOver() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
hover: true,
|
||||
});
|
||||
}
|
||||
|
||||
const text = editing ? (
|
||||
<div className="todo">
|
||||
<textarea
|
||||
className="todo--input"
|
||||
defaultValue={todo.text}
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ButtonBase
|
||||
style={{
|
||||
justifyContent: 'left',
|
||||
paddingLeft: '1rem',
|
||||
textDecoration: todo.completed ? 'line-through' : 'none',
|
||||
color: todo.completed ? '#888888' : 'black',
|
||||
}}
|
||||
className="todo"
|
||||
onClick={() => {
|
||||
toggleTodo();
|
||||
}}
|
||||
>
|
||||
{todo.text}
|
||||
</ButtonBase>
|
||||
);
|
||||
const ButtonBases = editing
|
||||
? [
|
||||
<ButtonBase
|
||||
key="save"
|
||||
style={{ backgroundColor: 'lightgreen' }}
|
||||
className="save"
|
||||
onClick={() => this.stopEdit(input.value)}
|
||||
>
|
||||
<CheckIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
]
|
||||
: [
|
||||
<ButtonBase
|
||||
key="remove"
|
||||
style={hover ? { backgroundColor: 'pink' } : disabledAction}
|
||||
className={deleteClasses.join(' ')}
|
||||
onClick={removeTodo}
|
||||
>
|
||||
<DeleteIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
<ButtonBase
|
||||
key="edit"
|
||||
style={hover ? { backgroundColor: 'lightcyan' } : disabledAction}
|
||||
className={editClasses.join(' ')}
|
||||
onClick={this.startEdit}
|
||||
>
|
||||
<EditIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
];
|
||||
return (
|
||||
<animated.li
|
||||
style={{
|
||||
...style,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
}}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onFocus={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
onBlur={this.onMouseOut}
|
||||
>
|
||||
{text}
|
||||
{ButtonBases}
|
||||
</animated.li>
|
||||
);
|
||||
}
|
||||
onMouseOut() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
|
||||
stopEdit(value) {
|
||||
const { editTodo } = this.props;
|
||||
editTodo(value);
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
editing: false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteClasses = ["delete"];
|
||||
const editClasses = ["edit"];
|
||||
const { hover, editing } = this.state;
|
||||
const { todo, removeTodo, toggleTodo, style } = this.props;
|
||||
if (!hover) {
|
||||
deleteClasses.push("disabled");
|
||||
editClasses.push("disabled");
|
||||
}
|
||||
|
||||
let input;
|
||||
|
||||
const text = editing ? (
|
||||
<div className="todo">
|
||||
<textarea
|
||||
className="todo--input"
|
||||
defaultValue={todo.text}
|
||||
ref={(node) => {
|
||||
input = node;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ButtonBase
|
||||
style={{
|
||||
justifyContent: "left",
|
||||
paddingLeft: "1rem",
|
||||
textDecoration: todo.completed ? "line-through" : "none",
|
||||
color: todo.completed ? "#888888" : "black",
|
||||
}}
|
||||
className="todo"
|
||||
onClick={() => {
|
||||
toggleTodo();
|
||||
}}
|
||||
>
|
||||
{todo.text}
|
||||
</ButtonBase>
|
||||
);
|
||||
const ButtonBases = editing
|
||||
? [
|
||||
<ButtonBase
|
||||
key="save"
|
||||
style={{ backgroundColor: "lightgreen" }}
|
||||
className="save"
|
||||
onClick={() => this.stopEdit(input.value)}
|
||||
>
|
||||
<CheckIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
]
|
||||
: [
|
||||
<ButtonBase
|
||||
key="remove"
|
||||
style={
|
||||
hover ? { backgroundColor: "pink" } : disabledAction
|
||||
}
|
||||
className={deleteClasses.join(" ")}
|
||||
onClick={removeTodo}
|
||||
>
|
||||
<DeleteIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
<ButtonBase
|
||||
key="edit"
|
||||
style={
|
||||
hover
|
||||
? { backgroundColor: "lightcyan" }
|
||||
: disabledAction
|
||||
}
|
||||
className={editClasses.join(" ")}
|
||||
onClick={this.startEdit}
|
||||
>
|
||||
<EditIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
];
|
||||
return (
|
||||
<animated.li
|
||||
style={{
|
||||
...style,
|
||||
borderTop: "1px solid #f0f0f0",
|
||||
}}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onFocus={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
onBlur={this.onMouseOut}
|
||||
>
|
||||
{text}
|
||||
{ButtonBases}
|
||||
</animated.li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Todo.propTypes = {
|
||||
todo: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
removeTodo: PropTypes.func.isRequired,
|
||||
toggleTodo: PropTypes.func.isRequired,
|
||||
editTodo: PropTypes.func.isRequired,
|
||||
style: PropTypes.shape({ height: PropTypes.object.isRequired }).isRequired,
|
||||
todo: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
removeTodo: PropTypes.func.isRequired,
|
||||
toggleTodo: PropTypes.func.isRequired,
|
||||
editTodo: PropTypes.func.isRequired,
|
||||
style: PropTypes.shape({ height: PropTypes.object.isRequired }).isRequired,
|
||||
};
|
||||
|
||||
export default Todo;
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition, config } from 'react-spring';
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Transition, config } from "react-spring";
|
||||
|
||||
import Todo from './Todo';
|
||||
import Todo from "./Todo";
|
||||
|
||||
export default function TodosContainer({
|
||||
todos,
|
||||
toggleTodo,
|
||||
removeTodo,
|
||||
editTodo,
|
||||
todos,
|
||||
toggleTodo,
|
||||
removeTodo,
|
||||
editTodo,
|
||||
}) {
|
||||
return (
|
||||
<ul id="list">
|
||||
<Transition
|
||||
native
|
||||
config={{
|
||||
...config.default,
|
||||
overshootClamping: true,
|
||||
restSpeedThreshold: 1,
|
||||
restDisplacementThreshold: 1,
|
||||
}}
|
||||
items={todos}
|
||||
keys={todo => todo.id}
|
||||
from={{
|
||||
height: 0,
|
||||
borderColor: '#f0f0f0',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
enter={{
|
||||
height: 60,
|
||||
borderColor: '#f0f0f0',
|
||||
opacity: 1,
|
||||
}}
|
||||
leave={{
|
||||
height: 0,
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 0,
|
||||
opacity: 0.3,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{todos.map(todo => styles => (
|
||||
<Todo
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
style={styles}
|
||||
toggleTodo={() => toggleTodo(todo.id)}
|
||||
removeTodo={() => removeTodo(todo.id)}
|
||||
editTodo={text => editTodo(todo.id, text)}
|
||||
/>
|
||||
))}
|
||||
</Transition>
|
||||
</ul>
|
||||
);
|
||||
return (
|
||||
<ul id="list">
|
||||
<Transition
|
||||
native
|
||||
config={{
|
||||
...config.default,
|
||||
overshootClamping: true,
|
||||
restSpeedThreshold: 1,
|
||||
restDisplacementThreshold: 1,
|
||||
}}
|
||||
items={todos}
|
||||
keys={(todo) => todo.id}
|
||||
from={{
|
||||
height: 0,
|
||||
borderColor: "#f0f0f0",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
enter={{
|
||||
height: 60,
|
||||
borderColor: "#f0f0f0",
|
||||
opacity: 1,
|
||||
}}
|
||||
leave={{
|
||||
height: 0,
|
||||
borderColor: "#ffffff",
|
||||
borderWidth: 0,
|
||||
opacity: 0.3,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
{todos.map((todo) => (styles) => (
|
||||
<Todo
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
style={styles}
|
||||
toggleTodo={() => toggleTodo(todo.id)}
|
||||
removeTodo={() => removeTodo(todo.id)}
|
||||
editTodo={(text) => editTodo(todo.id, text)}
|
||||
/>
|
||||
))}
|
||||
</Transition>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
TodosContainer.propTypes = {
|
||||
todos: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
removeTodo: PropTypes.func.isRequired,
|
||||
toggleTodo: PropTypes.func.isRequired,
|
||||
editTodo: PropTypes.func.isRequired,
|
||||
todos: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
removeTodo: PropTypes.func.isRequired,
|
||||
toggleTodo: PropTypes.func.isRequired,
|
||||
editTodo: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import { connect } from 'react-redux';
|
||||
import TodoList from './TodoList';
|
||||
import { toggleTodo, removeTodo, editTodo } from '../../actions/todos';
|
||||
import { connect } from "react-redux";
|
||||
import TodoList from "./TodoList";
|
||||
import { toggleTodo, removeTodo, editTodo } from "../../actions/todos";
|
||||
|
||||
import getVisibleTodos from './getVisibleTodos';
|
||||
import getVisibleTodos from "./getVisibleTodos";
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
todos: state.lists.list
|
||||
? getVisibleTodos(
|
||||
state.lists.lists[state.lists.list].todos.map(
|
||||
id => state.todos.todos[id],
|
||||
),
|
||||
state.visibilityFilter,
|
||||
)
|
||||
: [],
|
||||
dirty: state.todos.dirty,
|
||||
};
|
||||
return {
|
||||
todos: state.lists.list
|
||||
? getVisibleTodos(
|
||||
state.lists.lists[state.lists.list].todos.map(
|
||||
(id) => state.todos.todos[id],
|
||||
),
|
||||
state.visibilityFilter,
|
||||
)
|
||||
: [],
|
||||
dirty: state.todos.dirty,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
toggleTodo: id => dispatch(toggleTodo(id)),
|
||||
removeTodo: id => dispatch(removeTodo(id)),
|
||||
editTodo: (id, text) => dispatch(editTodo(id, text)),
|
||||
};
|
||||
return {
|
||||
toggleTodo: (id) => dispatch(toggleTodo(id)),
|
||||
removeTodo: (id) => dispatch(removeTodo(id)),
|
||||
editTodo: (id, text) => dispatch(editTodo(id, text)),
|
||||
};
|
||||
}
|
||||
|
||||
const TodosContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(TodoList);
|
||||
const TodosContainer = connect(mapStateToProps, mapDispatchToProps)(TodoList);
|
||||
|
||||
export default TodosContainer;
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition } from 'react-spring';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Transition } from "react-spring";
|
||||
|
||||
import withRouter from 'react-router-dom/withRouter';
|
||||
import Input from '../todos/Input';
|
||||
import TodoListContainer from './TodoListContainer';
|
||||
import Header from '../Header';
|
||||
import Filters from '../filters/Filters';
|
||||
import withRouter from "react-router-dom/withRouter";
|
||||
import Input from "../todos/Input";
|
||||
import TodoListContainer from "./TodoListContainer";
|
||||
import Header from "../Header";
|
||||
import Filters from "../filters/Filters";
|
||||
|
||||
class Todos extends React.PureComponent {
|
||||
render() {
|
||||
const { list } = this.props;
|
||||
return (
|
||||
<div id="todos">
|
||||
<Header />
|
||||
<Transition
|
||||
from={{ opacity: 0, maxHeight: 0 }}
|
||||
enter={{ opacity: 1, maxHeight: 38 }}
|
||||
leave={{ opacity: 0, maxHeight: 0 }}
|
||||
>
|
||||
{list && (styles => <Input styles={styles} />)}
|
||||
</Transition>
|
||||
<TodoListContainer />
|
||||
<Transition
|
||||
from={{ opacity: 0, maxHeight: 0 }}
|
||||
enter={{ opacity: 1, maxHeight: 32 }}
|
||||
leave={{ opacity: 0, maxHeight: 0 }}
|
||||
>
|
||||
{list && Filters}
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const { list } = this.props;
|
||||
return (
|
||||
<div id="todos">
|
||||
<Header />
|
||||
<Transition
|
||||
from={{ opacity: 0, maxHeight: 0 }}
|
||||
enter={{ opacity: 1, maxHeight: 38 }}
|
||||
leave={{ opacity: 0, maxHeight: 0 }}
|
||||
>
|
||||
{list && ((styles) => <Input 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 = {
|
||||
list: PropTypes.bool.isRequired,
|
||||
user: PropTypes.any.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
list: PropTypes.bool.isRequired,
|
||||
user: PropTypes.any.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
list: Boolean(state.lists.list),
|
||||
user: state.user,
|
||||
};
|
||||
return {
|
||||
list: Boolean(state.lists.list),
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Todos));
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { VisibilityFilters } from '../../actions/defs';
|
||||
import { VisibilityFilters } from "../../actions/defs";
|
||||
|
||||
export default function getVisibleTodos(todos, filter) {
|
||||
switch (filter) {
|
||||
case VisibilityFilters.SHOW_ALL:
|
||||
return todos.filter(todo => todo);
|
||||
case VisibilityFilters.SHOW_ACTIVE:
|
||||
return todos.filter(todo => todo).filter(todo => !todo.completed);
|
||||
case VisibilityFilters.SHOW_COMPLETED:
|
||||
return todos.filter(todo => todo).filter(todo => todo.completed);
|
||||
default:
|
||||
return todos.filter(todo => todo);
|
||||
}
|
||||
switch (filter) {
|
||||
case VisibilityFilters.SHOW_ALL:
|
||||
return todos.filter((todo) => todo);
|
||||
case VisibilityFilters.SHOW_ACTIVE:
|
||||
return todos
|
||||
.filter((todo) => todo)
|
||||
.filter((todo) => !todo.completed);
|
||||
case VisibilityFilters.SHOW_COMPLETED:
|
||||
return todos
|
||||
.filter((todo) => todo)
|
||||
.filter((todo) => todo.completed);
|
||||
default:
|
||||
return todos.filter((todo) => todo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,61 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@material-ui/core';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import { addTodo } from '../../actions/todos';
|
||||
import * as React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Button } from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import { addTodo } from "../../actions/todos";
|
||||
|
||||
function Input({ onClick, styles }) {
|
||||
let input;
|
||||
let input;
|
||||
|
||||
function submit() {
|
||||
if (input.value.trim() !== '') {
|
||||
onClick(input.value);
|
||||
function submit() {
|
||||
if (input.value.trim() !== "") {
|
||||
onClick(input.value);
|
||||
}
|
||||
input.value = "";
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles} id="inputs">
|
||||
<input
|
||||
aria-label="todo text"
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
id="input"
|
||||
type="text"
|
||||
placeholder="Add something!"
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
submit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button style={{ borderRadius: 0 }} id="add" onClick={() => submit()}>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={styles} id="inputs">
|
||||
<input
|
||||
aria-label="todo text"
|
||||
ref={(node) => {
|
||||
input = node;
|
||||
}}
|
||||
id="input"
|
||||
type="text"
|
||||
placeholder="Add something!"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
submit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ borderRadius: 0 }}
|
||||
id="add"
|
||||
onClick={() => submit()}
|
||||
>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Input.propTypes = {
|
||||
styles: PropTypes.any.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
styles: PropTypes.any.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return { ...ownProps };
|
||||
return { ...ownProps };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: text => dispatch(addTodo(text)),
|
||||
};
|
||||
return {
|
||||
onClick: (text) => dispatch(addTodo(text)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Input);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Input);
|
||||
|
||||
@@ -1,128 +1,123 @@
|
||||
import React from 'react';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase, Button } from '@material-ui/core';
|
||||
import React from "react";
|
||||
import { Field, reduxForm } from "redux-form";
|
||||
import { connect } from "react-redux";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { ButtonBase, Button } from "@material-ui/core";
|
||||
|
||||
import InputField from './InputField';
|
||||
import UserErrors from './UserErrors';
|
||||
import InputField from "./InputField";
|
||||
import UserErrors from "./UserErrors";
|
||||
|
||||
import './Form.css';
|
||||
import "./Form.css";
|
||||
|
||||
import { edit, resetEdit, deleteUser } from '../../actions/user';
|
||||
import { edit, resetEdit, deleteUser } from "../../actions/user";
|
||||
|
||||
function validate(values) {
|
||||
const errors = {};
|
||||
if (values.password !== values.passwordRepeat) {
|
||||
errors.passwordRepeat = 'Passwords should match';
|
||||
}
|
||||
return errors;
|
||||
const errors = {};
|
||||
if (values.password !== values.passwordRepeat) {
|
||||
errors.passwordRepeat = "Passwords should match";
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function EditForm({
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
deleteUser,
|
||||
user,
|
||||
history,
|
||||
reset,
|
||||
handleSubmit,
|
||||
onSubmit,
|
||||
deleteUser,
|
||||
user,
|
||||
history,
|
||||
reset,
|
||||
}) {
|
||||
if (user.user && user.editSuccess) {
|
||||
reset();
|
||||
history.push('/');
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: '0',
|
||||
marginRight: 'auto',
|
||||
padding: '0 0.5rem',
|
||||
}}
|
||||
onClick={() => {
|
||||
history.push('/');
|
||||
}}
|
||||
>
|
||||
todos
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
label="repeat pasword"
|
||||
name="passwordRepeat"
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<div id="buttons">
|
||||
<Button onClick={() => deleteUser()}>Delete your account</Button>
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
if (user.user && user.editSuccess) {
|
||||
reset();
|
||||
history.push("/");
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: "0",
|
||||
marginRight: "auto",
|
||||
padding: "0 0.5rem",
|
||||
}}
|
||||
onClick={() => {
|
||||
history.push("/");
|
||||
}}
|
||||
>
|
||||
todos
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
label="repeat pasword"
|
||||
name="passwordRepeat"
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<div id="buttons">
|
||||
<Button onClick={() => deleteUser()}>
|
||||
Delete your account
|
||||
</Button>
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
EditForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
deleteUser: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
deleteUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
reset: () => dispatch(resetEdit()),
|
||||
deleteUser: () => dispatch(deleteUser()),
|
||||
onSubmit: ({ username, password }) =>
|
||||
dispatch(edit({ username, password })),
|
||||
};
|
||||
return {
|
||||
reset: () => dispatch(resetEdit()),
|
||||
deleteUser: () => dispatch(deleteUser()),
|
||||
onSubmit: ({ username, password }) =>
|
||||
dispatch(edit({ username, password })),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'editForm',
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
},
|
||||
validate,
|
||||
})(
|
||||
withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(EditForm),
|
||||
),
|
||||
);
|
||||
form: "editForm",
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
passwordRepeat: "",
|
||||
},
|
||||
validate,
|
||||
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(EditForm)));
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
#form {
|
||||
margin: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 20rem;
|
||||
max-width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 1rem;
|
||||
color: red;
|
||||
margin: 1rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
#googlebutton {
|
||||
margin: auto;
|
||||
margin-left: 0rem;
|
||||
margin: auto;
|
||||
margin-left: 0rem;
|
||||
}
|
||||
|
||||
#submitbutton {
|
||||
margin: auto;
|
||||
margin-right: 0rem;
|
||||
margin: auto;
|
||||
margin-right: 0rem;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 0 0.5rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { ButtonBase } from "@material-ui/core";
|
||||
|
||||
function Link({ history, to, text }) {
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: '0',
|
||||
marginRight: 'auto',
|
||||
padding: '0 1rem',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
history.push(to);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</ButtonBase>
|
||||
);
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: "0",
|
||||
marginRight: "auto",
|
||||
padding: "0 1rem",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
history.push(to);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
history: PropTypes.any,
|
||||
to: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
history: PropTypes.any,
|
||||
to: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(Link);
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextField } from '@material-ui/core';
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { TextField } from "@material-ui/core";
|
||||
|
||||
export default function InputField({
|
||||
required,
|
||||
input,
|
||||
label,
|
||||
meta: { touched, error },
|
||||
type,
|
||||
required,
|
||||
input,
|
||||
label,
|
||||
meta: { touched, error },
|
||||
type,
|
||||
}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label={label}
|
||||
required={required}
|
||||
{...input}
|
||||
type={type}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
/>
|
||||
{touched && error && <span className="error">{error}</span>}
|
||||
</React.Fragment>
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label={label}
|
||||
required={required}
|
||||
{...input}
|
||||
type={type}
|
||||
style={{ marginBottom: "1rem" }}
|
||||
/>
|
||||
{touched && error && <span className="error">{error}</span>}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
InputField.propTypes = {
|
||||
required: PropTypes.bool.isRequired,
|
||||
input: PropTypes.any.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
meta: PropTypes.shape({
|
||||
touched: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
}).isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
required: PropTypes.bool.isRequired,
|
||||
input: PropTypes.any.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
meta: PropTypes.shape({
|
||||
touched: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
}).isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,125 +1,118 @@
|
||||
import React from 'react';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase, Button } from '@material-ui/core';
|
||||
import { withRouter } from 'react-router';
|
||||
import React from "react";
|
||||
import { Field, reduxForm } from "redux-form";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { ButtonBase, Button } from "@material-ui/core";
|
||||
import { withRouter } from "react-router";
|
||||
|
||||
import InputField from './InputField';
|
||||
import UserErrors from './UserErrors';
|
||||
import InputField from "./InputField";
|
||||
import UserErrors from "./UserErrors";
|
||||
|
||||
import './Form.css';
|
||||
import "./Form.css";
|
||||
|
||||
import { login, reset, loginJWT } from '../../actions/user';
|
||||
import { login, reset, loginJWT } from "../../actions/user";
|
||||
|
||||
class LoginForm extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { setJWT } = this.props;
|
||||
const params = new URLSearchParams(new URL(window.location).search);
|
||||
if (params.has('jwt')) {
|
||||
const jwt = params.get('jwt');
|
||||
setJWT(jwt);
|
||||
componentDidMount() {
|
||||
const { setJWT } = this.props;
|
||||
const params = new URLSearchParams(new URL(window.location).search);
|
||||
if (params.has("jwt")) {
|
||||
const jwt = params.get("jwt");
|
||||
setJWT(jwt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { resetUser, history, handleSubmit, user, onLogin } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: '1rem',
|
||||
padding: '0 0.5rem',
|
||||
borderRadius: '7px',
|
||||
}}
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push('/signup');
|
||||
}}
|
||||
>
|
||||
signup
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onLogin)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
render() {
|
||||
const { resetUser, history, handleSubmit, user, onLogin } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: "1rem",
|
||||
padding: "0 0.5rem",
|
||||
borderRadius: "7px",
|
||||
}}
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push("/signup");
|
||||
}}
|
||||
>
|
||||
signup
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onLogin)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div id="buttons">
|
||||
<Button
|
||||
id="googlebutton"
|
||||
variant="raised"
|
||||
onClick={() => {
|
||||
window.location = '/__/users/login/google/';
|
||||
}}
|
||||
>
|
||||
Google
|
||||
</Button>
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
<div id="buttons">
|
||||
<Button
|
||||
id="googlebutton"
|
||||
variant="raised"
|
||||
onClick={() => {
|
||||
window.location = "/__/users/login/google/";
|
||||
}}
|
||||
>
|
||||
Google
|
||||
</Button>
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
setJWT: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
setJWT: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onLogin: ({ username, password }) =>
|
||||
dispatch(login({ username, password })),
|
||||
setJWT: jwt => dispatch(loginJWT(jwt)),
|
||||
};
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onLogin: ({ username, password }) =>
|
||||
dispatch(login({ username, password })),
|
||||
setJWT: (jwt) => dispatch(loginJWT(jwt)),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'loginForm',
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
})(
|
||||
withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(LoginForm)
|
||||
),
|
||||
);
|
||||
form: "loginForm",
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm)));
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { ButtonBase } from "@material-ui/core";
|
||||
|
||||
import { logout } from '../../actions/user';
|
||||
import { logout } from "../../actions/user";
|
||||
|
||||
function Link({ onClick, children }) {
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 0,
|
||||
padding: '0 1rem',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
marginRight: 0,
|
||||
padding: "0 1rem",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: () => dispatch(logout()),
|
||||
};
|
||||
return {
|
||||
onClick: () => dispatch(logout()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(Link);
|
||||
export default connect(null, mapDispatchToProps)(Link);
|
||||
|
||||
@@ -1,118 +1,111 @@
|
||||
import React from 'react';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase, Button } from '@material-ui/core';
|
||||
import { withRouter } from 'react-router';
|
||||
import React from "react";
|
||||
import { Field, reduxForm } from "redux-form";
|
||||
import { connect } from "react-redux";
|
||||
import PropTypes from "prop-types";
|
||||
import { ButtonBase, Button } from "@material-ui/core";
|
||||
import { withRouter } from "react-router";
|
||||
|
||||
import InputField from './InputField';
|
||||
import UserErrors from './UserErrors';
|
||||
import InputField from "./InputField";
|
||||
import UserErrors from "./UserErrors";
|
||||
|
||||
import './Form.css';
|
||||
import "./Form.css";
|
||||
|
||||
import { signup, reset } from '../../actions/user';
|
||||
import { signup, reset } from "../../actions/user";
|
||||
|
||||
function validate(values) {
|
||||
const errors = {};
|
||||
if (values.password !== values.passwordRepeat) {
|
||||
errors.passwordRepeat = 'Passwords should match';
|
||||
}
|
||||
return errors;
|
||||
const errors = {};
|
||||
if (values.password !== values.passwordRepeat) {
|
||||
errors.passwordRepeat = "Passwords should match";
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function SignupForm({ handleSubmit, onSignup, user, history, resetUser }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: '1rem',
|
||||
padding: '0 0.5rem',
|
||||
borderRadius: '7px',
|
||||
}}
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push('/login');
|
||||
}}
|
||||
>
|
||||
login
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onSignup)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
label="repeat pasword"
|
||||
name="passwordRepeat"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<div id="buttons">
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Signup
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: "1rem",
|
||||
padding: "0 0.5rem",
|
||||
borderRadius: "7px",
|
||||
}}
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push("/login");
|
||||
}}
|
||||
>
|
||||
login
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onSignup)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
label="repeat pasword"
|
||||
name="passwordRepeat"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<div id="buttons">
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Signup
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
SignupForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onSignup: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onSignup: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onSignup: ({ username, password }) =>
|
||||
dispatch(signup({ username, password })),
|
||||
};
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onSignup: ({ username, password }) =>
|
||||
dispatch(signup({ username, password })),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'signupForm',
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
},
|
||||
validate,
|
||||
})(
|
||||
withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(SignupForm)
|
||||
),
|
||||
);
|
||||
form: "signupForm",
|
||||
initialValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
passwordRepeat: "",
|
||||
},
|
||||
validate,
|
||||
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(SignupForm)));
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
function UserErrors({ user }) {
|
||||
const errors = [];
|
||||
if (user.errors) {
|
||||
if (user.errors.name === 'AuthenticationError') {
|
||||
errors.push(
|
||||
<div key="wrongauth" className="error">
|
||||
Wrong username or password
|
||||
</div>,
|
||||
);
|
||||
const errors = [];
|
||||
if (user.errors) {
|
||||
if (user.errors.name === "AuthenticationError") {
|
||||
errors.push(
|
||||
<div key="wrongauth" className="error">
|
||||
Wrong username or password
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (user.errors.name === "ValidationError") {
|
||||
if (user.errors.message.split(" ").includes("unique.")) {
|
||||
errors.push(
|
||||
<div key="exists" className="error">
|
||||
User already exists
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
<div key="invalid" className="error">
|
||||
Validation error
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (user.errors.name === 'ValidationError') {
|
||||
if (user.errors.message.split(' ').includes('unique.')) {
|
||||
errors.push(
|
||||
<div key="exists" className="error">
|
||||
User already exists
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
<div key="invalid" className="error">
|
||||
Validation error
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors || null;
|
||||
return errors || null;
|
||||
}
|
||||
|
||||
export default UserErrors;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import LogoutLink from './LogoutLink';
|
||||
import HeaderLink from './HeaderLink';
|
||||
import LogoutLink from "./LogoutLink";
|
||||
import HeaderLink from "./HeaderLink";
|
||||
|
||||
export default function UserHeader() {
|
||||
return (
|
||||
<div id="user-header">
|
||||
<HeaderLink to="/edit" text="account"/>
|
||||
<LogoutLink>logout</LogoutLink>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div id="user-header">
|
||||
<HeaderLink to="/edit" text="account" />
|
||||
<LogoutLink>logout</LogoutLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Provider } from 'react-redux';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import { offline } from '@redux-offline/redux-offline';
|
||||
import offlineConfig from '@redux-offline/redux-offline/lib/defaults';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import thunk from "redux-thunk";
|
||||
import { Provider } from "react-redux";
|
||||
import { applyMiddleware, createStore, compose } from "redux";
|
||||
import { offline } from "@redux-offline/redux-offline";
|
||||
import offlineConfig from "@redux-offline/redux-offline/lib/defaults";
|
||||
|
||||
import AppContainer from './components/AppContainer';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import todoApp from './reducers';
|
||||
import { setToken } from './actions/util';
|
||||
import keepSynced from './middleware/keepSynced';
|
||||
import AppContainer from "./components/AppContainer";
|
||||
import registerServiceWorker from "./registerServiceWorker";
|
||||
import todoApp from "./reducers";
|
||||
import { setToken } from "./actions/util";
|
||||
import keepSynced from "./middleware/keepSynced";
|
||||
|
||||
let store;
|
||||
|
||||
const persistCallback = () => {
|
||||
const state = store.getState();
|
||||
if (state.user.user) {
|
||||
setToken(state.user.user.jwt);
|
||||
}
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<AppContainer />
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
const state = store.getState();
|
||||
if (state.user.user) {
|
||||
setToken(state.user.user.jwt);
|
||||
}
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<AppContainer />
|
||||
</Provider>,
|
||||
document.getElementById("root"),
|
||||
);
|
||||
};
|
||||
|
||||
store = createStore(
|
||||
todoApp,
|
||||
compose(
|
||||
offline({ ...offlineConfig, persistCallback }),
|
||||
applyMiddleware(thunk, keepSynced),
|
||||
),
|
||||
todoApp,
|
||||
compose(
|
||||
offline({ ...offlineConfig, persistCallback }),
|
||||
applyMiddleware(thunk, keepSynced),
|
||||
),
|
||||
);
|
||||
|
||||
registerServiceWorker();
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { REQUEST_LISTS, INVALIDATE_LISTS } from '../actions/defs';
|
||||
import { fetchLists } from '../actions/lists';
|
||||
import { REQUEST_LISTS, INVALIDATE_LISTS } from "../actions/defs";
|
||||
import { fetchLists } from "../actions/lists";
|
||||
|
||||
export default store => next => action => {
|
||||
next(action);
|
||||
if (action.type !== REQUEST_LISTS && typeof action !== 'function') {
|
||||
const state = store.getState();
|
||||
if (state.user.user) {
|
||||
const dirtyLists = state.lists.dirty || false;
|
||||
const dirtyTodos = state.todos.dirty || false;
|
||||
const fetchingLists = state.lists.fetching || false;
|
||||
if (
|
||||
((dirtyLists || dirtyTodos) && !fetchingLists) ||
|
||||
action.type === INVALIDATE_LISTS
|
||||
) {
|
||||
store.dispatch(fetchLists());
|
||||
}
|
||||
export default (store) => (next) => (action) => {
|
||||
next(action);
|
||||
if (action.type !== REQUEST_LISTS && typeof action !== "function") {
|
||||
const state = store.getState();
|
||||
if (state.user.user) {
|
||||
const dirtyLists = state.lists.dirty || false;
|
||||
const dirtyTodos = state.todos.dirty || false;
|
||||
const fetchingLists = state.lists.fetching || false;
|
||||
if (
|
||||
((dirtyLists || dirtyTodos) && !fetchingLists) ||
|
||||
action.type === INVALIDATE_LISTS
|
||||
) {
|
||||
store.dispatch(fetchLists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
import { combineReducers } from "redux";
|
||||
import { reducer as formReducer } from "redux-form";
|
||||
|
||||
import lists from './lists';
|
||||
import visibilityFilter from './visibilityFilter';
|
||||
import user from './user';
|
||||
import todos from './todos';
|
||||
import lists from "./lists";
|
||||
import visibilityFilter from "./visibilityFilter";
|
||||
import user from "./user";
|
||||
import todos from "./todos";
|
||||
|
||||
const todoApp = combineReducers({
|
||||
lists,
|
||||
todos,
|
||||
visibilityFilter,
|
||||
form: formReducer,
|
||||
user,
|
||||
lists,
|
||||
todos,
|
||||
visibilityFilter,
|
||||
form: formReducer,
|
||||
user,
|
||||
});
|
||||
|
||||
export default todoApp;
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
import {
|
||||
CHANGE_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
VALIDATE_LISTS,
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
ADD_LIST,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
STOP_CREATE_LIST,
|
||||
STOP_EDIT_LIST,
|
||||
REMOVE_TODO,
|
||||
ADD_TODO,
|
||||
LOGOUT,
|
||||
} from '../actions/defs';
|
||||
CHANGE_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
VALIDATE_LISTS,
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
ADD_LIST,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
STOP_CREATE_LIST,
|
||||
STOP_EDIT_LIST,
|
||||
REMOVE_TODO,
|
||||
ADD_TODO,
|
||||
LOGOUT,
|
||||
} from "../actions/defs";
|
||||
|
||||
export default function lists(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
lists: null,
|
||||
loaded: false,
|
||||
creating: false,
|
||||
list: null,
|
||||
editing: false,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOGOUT:
|
||||
return {
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
lists: null,
|
||||
@@ -38,123 +25,141 @@ export default function lists(
|
||||
creating: false,
|
||||
list: null,
|
||||
editing: false,
|
||||
};
|
||||
case CHANGE_LIST:
|
||||
return { ...state, list: action.list };
|
||||
case RECIEVE_LISTS: {
|
||||
const newLists = Object.values(action.lists);
|
||||
let { list } = state;
|
||||
if (newLists.length !== 0) {
|
||||
if (!newLists.some(curList => curList.id === list)) {
|
||||
list = newLists[0].id || null;
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOGOUT:
|
||||
return {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
lists: null,
|
||||
loaded: false,
|
||||
creating: false,
|
||||
list: null,
|
||||
editing: false,
|
||||
};
|
||||
case CHANGE_LIST:
|
||||
return { ...state, list: action.list };
|
||||
case RECIEVE_LISTS: {
|
||||
const newLists = Object.values(action.lists);
|
||||
let { list } = state;
|
||||
if (newLists.length !== 0) {
|
||||
if (!newLists.some((curList) => curList.id === list)) {
|
||||
list = newLists[0].id || null;
|
||||
}
|
||||
} else {
|
||||
list = null;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
loaded: true,
|
||||
fetching: false,
|
||||
lists: action.lists,
|
||||
list,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
list = null;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
loaded: true,
|
||||
fetching: false,
|
||||
lists: action.lists,
|
||||
list,
|
||||
};
|
||||
case START_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: true,
|
||||
};
|
||||
case STOP_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
};
|
||||
case ADD_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
lists: { ...state.lists, [action.list.id]: action.list },
|
||||
list: action.list.id,
|
||||
};
|
||||
case REMOVE_LIST: {
|
||||
const newLists = { ...state.lists };
|
||||
delete newLists[action.list];
|
||||
const listsObjs = Object.values(newLists);
|
||||
const list = listsObjs.length
|
||||
? listsObjs[listsObjs.length - 1].id
|
||||
: null;
|
||||
return {
|
||||
...state,
|
||||
list,
|
||||
lists: newLists,
|
||||
};
|
||||
}
|
||||
case START_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
editing: true,
|
||||
};
|
||||
}
|
||||
case STOP_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
case EDIT_LIST_NAME: {
|
||||
return {
|
||||
...state,
|
||||
editing: false,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[action.list]: {
|
||||
...state.lists[action.list],
|
||||
name: action.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case REMOVE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
todos: state.lists[state.list].todos.filter(
|
||||
(todo) => todo !== action.id,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ADD_TODO: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
todos: [
|
||||
action.todo.id,
|
||||
...state.lists[state.list].todos,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case INVALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case REQUEST_LISTS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
case START_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: true,
|
||||
};
|
||||
case STOP_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
};
|
||||
case ADD_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
lists: { ...state.lists, [action.list.id]: action.list },
|
||||
list: action.list.id,
|
||||
};
|
||||
case REMOVE_LIST: {
|
||||
const newLists = { ...state.lists };
|
||||
delete newLists[action.list];
|
||||
const listsObjs = Object.values(newLists);
|
||||
const list = listsObjs.length ? listsObjs[listsObjs.length - 1].id : null;
|
||||
return {
|
||||
...state,
|
||||
list,
|
||||
lists: newLists,
|
||||
};
|
||||
}
|
||||
case START_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
editing: true,
|
||||
};
|
||||
}
|
||||
case STOP_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
case EDIT_LIST_NAME: {
|
||||
return {
|
||||
...state,
|
||||
editing: false,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[action.list]: {
|
||||
...state.lists[action.list],
|
||||
name: action.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case REMOVE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
todos: state.lists[state.list].todos.filter(
|
||||
todo => todo !== action.id,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ADD_TODO: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
todos: [action.todo.id, ...state.lists[state.list].todos],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case INVALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case REQUEST_LISTS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,102 @@
|
||||
import {
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
RECIEVE_TODOS,
|
||||
REQUEST_TODOS,
|
||||
INVALIDATE_TODOS,
|
||||
VALIDATE_TODOS,
|
||||
EDIT_TODO,
|
||||
REMOVE_LIST,
|
||||
LOGOUT,
|
||||
} from '../actions/defs';
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
RECIEVE_TODOS,
|
||||
REQUEST_TODOS,
|
||||
INVALIDATE_TODOS,
|
||||
VALIDATE_TODOS,
|
||||
EDIT_TODO,
|
||||
REMOVE_LIST,
|
||||
LOGOUT,
|
||||
} from "../actions/defs";
|
||||
|
||||
export default function todos(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOGOUT:
|
||||
return {
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
};
|
||||
case RECIEVE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
todos: action.todos,
|
||||
};
|
||||
case ADD_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: { [action.todo.id]: action.todo, ...state.todos },
|
||||
};
|
||||
case INVALIDATE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case EDIT_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[action.id]: { ...state.todos[action.id], text: action.text },
|
||||
},
|
||||
};
|
||||
case REQUEST_TODOS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case REMOVE_TODO: {
|
||||
const newTodos = { ...state.todos };
|
||||
delete newTodos[action.id];
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos,
|
||||
};
|
||||
}
|
||||
case REMOVE_LIST: {
|
||||
const newTodos = { ...state.todos };
|
||||
Object.keys(newTodos).forEach(todoId => {
|
||||
if (newTodos[todoId].list === action.list) {
|
||||
delete newTodos[todoId];
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOGOUT:
|
||||
return {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
};
|
||||
case RECIEVE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
todos: action.todos,
|
||||
};
|
||||
case ADD_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: { [action.todo.id]: action.todo, ...state.todos },
|
||||
};
|
||||
case INVALIDATE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case EDIT_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[action.id]: {
|
||||
...state.todos[action.id],
|
||||
text: action.text,
|
||||
},
|
||||
},
|
||||
};
|
||||
case REQUEST_TODOS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case REMOVE_TODO: {
|
||||
const newTodos = { ...state.todos };
|
||||
delete newTodos[action.id];
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos,
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos,
|
||||
};
|
||||
case REMOVE_LIST: {
|
||||
const newTodos = { ...state.todos };
|
||||
Object.keys(newTodos).forEach((todoId) => {
|
||||
if (newTodos[todoId].list === action.list) {
|
||||
delete newTodos[todoId];
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos,
|
||||
};
|
||||
}
|
||||
case TOGGLE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[action.id]: {
|
||||
...state.todos[action.id],
|
||||
completed: !state.todos[action.id].completed,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
case TOGGLE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[action.id]: {
|
||||
...state.todos[action.id],
|
||||
completed: !state.todos[action.id].completed,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +1,90 @@
|
||||
import {
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
START_LOGIN,
|
||||
LOGOUT,
|
||||
SIGNUP_FAIL,
|
||||
SIGNUP_SUCCESS,
|
||||
VALIDATE_USER,
|
||||
RESET_USER,
|
||||
EDIT_SUCCESS,
|
||||
EDIT_FAIL,
|
||||
RESET_EDIT,
|
||||
} from '../actions/defs';
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
START_LOGIN,
|
||||
LOGOUT,
|
||||
SIGNUP_FAIL,
|
||||
SIGNUP_SUCCESS,
|
||||
VALIDATE_USER,
|
||||
RESET_USER,
|
||||
EDIT_SUCCESS,
|
||||
EDIT_FAIL,
|
||||
RESET_EDIT,
|
||||
} from "../actions/defs";
|
||||
|
||||
export default function user(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
user: null,
|
||||
loaded: false,
|
||||
errors: null,
|
||||
},
|
||||
action,
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
user: null,
|
||||
loaded: false,
|
||||
errors: null,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case VALIDATE_USER:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case START_LOGIN:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case SIGNUP_SUCCESS:
|
||||
case LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
errors: null,
|
||||
dirty: false,
|
||||
loaded: true,
|
||||
fetching: false,
|
||||
};
|
||||
case EDIT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
editSuccess: true,
|
||||
};
|
||||
case SIGNUP_FAIL:
|
||||
case LOGIN_FAIL:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
errors: action.error,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
loaded: false,
|
||||
};
|
||||
case EDIT_FAIL:
|
||||
return {
|
||||
...state,
|
||||
errors: action.error,
|
||||
editSuccess: false,
|
||||
};
|
||||
case RESET_EDIT:
|
||||
return {
|
||||
...state,
|
||||
editSuccess: null,
|
||||
};
|
||||
case RESET_USER:
|
||||
return {
|
||||
...state,
|
||||
fetching: false,
|
||||
loaded: false,
|
||||
user: null,
|
||||
errors: null,
|
||||
};
|
||||
case LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
loaded: false,
|
||||
user: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case VALIDATE_USER:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case START_LOGIN:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case SIGNUP_SUCCESS:
|
||||
case LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
errors: null,
|
||||
dirty: false,
|
||||
loaded: true,
|
||||
fetching: false,
|
||||
};
|
||||
case EDIT_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
editSuccess: true,
|
||||
};
|
||||
case SIGNUP_FAIL:
|
||||
case LOGIN_FAIL:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
errors: action.error,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
loaded: false,
|
||||
};
|
||||
case EDIT_FAIL:
|
||||
return {
|
||||
...state,
|
||||
errors: action.error,
|
||||
editSuccess: false,
|
||||
};
|
||||
case RESET_EDIT:
|
||||
return {
|
||||
...state,
|
||||
editSuccess: null,
|
||||
};
|
||||
case RESET_USER:
|
||||
return {
|
||||
...state,
|
||||
fetching: false,
|
||||
loaded: false,
|
||||
user: null,
|
||||
errors: null,
|
||||
};
|
||||
case LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
loaded: false,
|
||||
user: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions/defs';
|
||||
import { VisibilityFilters, SET_VISIBILITY_FILTER } from "../actions/defs";
|
||||
|
||||
const { SHOW_ALL } = VisibilityFilters;
|
||||
|
||||
export default function visibilityFilter(state = SHOW_ALL, action) {
|
||||
switch (action.type) {
|
||||
case SET_VISIBILITY_FILTER:
|
||||
return action.filter;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
switch (action.type) {
|
||||
case SET_VISIBILITY_FILTER:
|
||||
return action.filter;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,110 +9,113 @@
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
window.location.hostname === "localhost" ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === "[::1]" ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
|
||||
),
|
||||
);
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log('New content is available; please refresh.');
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === "installed") {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log(
|
||||
"New content is available; please refresh.",
|
||||
);
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log("Content is cached for offline use.");
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error during service worker registration:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get("content-type").indexOf("javascript") ===
|
||||
-1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode.",
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
window.addEventListener("load", () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ',
|
||||
);
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
"This web app is being served cache-first by a service " +
|
||||
"worker. To learn more, visit https://goo.gl/SC7cgQ",
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(createProxyMiddleware('/__', { target: 'http://localhost:4000/' }));
|
||||
module.exports = function (app) {
|
||||
app.use(createProxyMiddleware("/__", { target: "http://localhost:4000/" }));
|
||||
};
|
||||
|
||||
18
config/db.js
18
config/db.js
@@ -1,18 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const config = require('./');
|
||||
|
||||
async function connect() {
|
||||
await mongoose.connect(config.db.uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
const env = process.env.NODE_ENV;
|
||||
|
||||
const production = {
|
||||
app: {
|
||||
port: process.env.PORT || 4000,
|
||||
},
|
||||
db: {
|
||||
uri:
|
||||
process.env.DB_URI ||
|
||||
process.env.MONGODB_URI ||
|
||||
'mongodb://localhost/todolist',
|
||||
},
|
||||
googleOAuth: {
|
||||
googleEnabled:
|
||||
process.env.GOOGLE_ENABLED ? process.env.GOOGLE_ENABLED.toUpperCase() === 'TRUE' : false,
|
||||
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
||||
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
googleCallback: `${process.env.HOST}/__/users/login/google/callback`,
|
||||
},
|
||||
secret: process.env.SECRET,
|
||||
};
|
||||
|
||||
const development = {
|
||||
...production,
|
||||
secret: process.env.SECRET || 'devsecret',
|
||||
};
|
||||
|
||||
const test = {
|
||||
...production,
|
||||
secret: process.env.SECRET || 'testsecret',
|
||||
};
|
||||
|
||||
const config = {
|
||||
production,
|
||||
development,
|
||||
test,
|
||||
};
|
||||
|
||||
module.exports = config[env] || config.production;
|
||||
@@ -1,31 +0,0 @@
|
||||
const passport = require('passport');
|
||||
const mongoose = require('mongoose');
|
||||
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
|
||||
const {
|
||||
googleClientId,
|
||||
googleClientSecret,
|
||||
googleCallback,
|
||||
googleEnabled,
|
||||
} = require('./').googleOAuth;
|
||||
|
||||
const User = mongoose.model('User');
|
||||
|
||||
passport.use(User.createStrategy());
|
||||
|
||||
if (googleEnabled) {
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: googleClientId,
|
||||
clientSecret: googleClientSecret,
|
||||
callbackURL: googleCallback,
|
||||
},
|
||||
(accessToken, refreshToken, profile, done) => {
|
||||
User.findOrCreate({ googleId: profile.id }, (err, user) =>
|
||||
done(err, user),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
module.exports = passport;
|
||||
@@ -1,21 +0,0 @@
|
||||
class NotFoundError extends Error {
|
||||
constructor(text, ...args) {
|
||||
super(...args);
|
||||
Error.captureStackTrace(this, NotFoundError);
|
||||
this.name = 'NotFound';
|
||||
this.text = text;
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class BadRequestError extends Error {
|
||||
constructor(text, ...args) {
|
||||
super(...args);
|
||||
Error.captureStackTrace(this, NotFoundError);
|
||||
this.name = 'BadRequest';
|
||||
this.text = text;
|
||||
this.status = 400;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { NotFoundError, BadRequestError };
|
||||
@@ -1,50 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const TodoSchema = Schema({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
minLength: 1,
|
||||
maxLength: 300,
|
||||
trim: true,
|
||||
},
|
||||
list: { type: Schema.Types.ObjectId, ref: 'TodoList', required: true },
|
||||
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
completed: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
TodoSchema.pre('save', async function() {
|
||||
if (this.isNew) {
|
||||
const user = await this.model('User').findById(this.user);
|
||||
user.todos.push(this._id);
|
||||
await user.save();
|
||||
|
||||
const list = await this.model('TodoList').findById(this.list);
|
||||
list.todos.push(this._id);
|
||||
await list.save();
|
||||
}
|
||||
});
|
||||
|
||||
TodoSchema.pre('remove', async function() {
|
||||
const user = await this.model('User').findById(this.user);
|
||||
user.todos.splice(user.todos.indexOf(this._id), 1);
|
||||
await user.save();
|
||||
|
||||
const list = await this.model('TodoList').findById(this.list);
|
||||
list.todos.splice(list.todos.indexOf(this._id), 1);
|
||||
await list.save();
|
||||
});
|
||||
|
||||
TodoSchema.methods.toJson = function() {
|
||||
return {
|
||||
id: this._id.toString(),
|
||||
text: this.text,
|
||||
list: this.list.toString(),
|
||||
user: this.user.toString(),
|
||||
completed: this.completed,
|
||||
};
|
||||
};
|
||||
|
||||
mongoose.model('Todo', TodoSchema);
|
||||
@@ -1,56 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const TodoListSchema = Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
minLength: 1,
|
||||
maxLength: 100,
|
||||
trim: true,
|
||||
},
|
||||
todos: [{ type: Schema.Types.ObjectId, ref: 'Todo' }],
|
||||
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
});
|
||||
|
||||
TodoListSchema.pre('save', async function() {
|
||||
if (this.isNew) {
|
||||
const user = await this.model('User').findById(this.user);
|
||||
user.lists.push(this._id);
|
||||
await user.save();
|
||||
}
|
||||
});
|
||||
|
||||
TodoListSchema.pre('remove', async function() {
|
||||
const user = await this.model('User').findById(this.user);
|
||||
user.lists.splice(user.lists.indexOf(this._id), 1);
|
||||
|
||||
// removing todos in parallel can cause VersionError
|
||||
// so we remove todos from user
|
||||
const todos = await this.model('Todo')
|
||||
.find({ list: this._id })
|
||||
.exec();
|
||||
const ids = todos.map(todo => todo._id);
|
||||
user.todos = user.todos.filter(todo => ids.includes(todo._id));
|
||||
await user.save();
|
||||
// and remove them from db
|
||||
await this.model('Todo')
|
||||
.find({ list: this._id })
|
||||
.remove()
|
||||
.exec();
|
||||
});
|
||||
|
||||
TodoListSchema.methods.toJson = function() {
|
||||
const todos = this.populated('todos')
|
||||
? this.todos.map(todo => todo.toJson())
|
||||
: this.todos;
|
||||
return {
|
||||
id: this._id.toString(),
|
||||
user: this.user.toString(),
|
||||
name: this.name,
|
||||
todos,
|
||||
};
|
||||
};
|
||||
|
||||
mongoose.model('TodoList', TodoListSchema);
|
||||
@@ -1,76 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const passportLocalMongoose = require('passport-local-mongoose');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const uniqueValidator = require('mongoose-unique-validator');
|
||||
const findOrCreate = require('mongoose-findorcreate');
|
||||
const { BadRequestError } = require('../errors');
|
||||
|
||||
const { secret } = require('../config');
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const UserSchema = Schema({
|
||||
username: {
|
||||
type: String,
|
||||
unique: true,
|
||||
validate: /^\S*$/,
|
||||
minLength: 3,
|
||||
maxLength: 50,
|
||||
trim: true,
|
||||
sparse: true,
|
||||
},
|
||||
googleId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
lists: [{ type: Schema.Types.ObjectId, ref: 'TodoList' }],
|
||||
todos: [{ type: Schema.Types.ObjectId, ref: 'Todo' }],
|
||||
});
|
||||
|
||||
UserSchema.plugin(passportLocalMongoose, {
|
||||
limitAttempts: true,
|
||||
maxAttempts: 20,
|
||||
});
|
||||
UserSchema.plugin(uniqueValidator);
|
||||
UserSchema.plugin(findOrCreate);
|
||||
|
||||
UserSchema.pre('validate', async function() {
|
||||
if (!this.username && !this.googleId) {
|
||||
throw new BadRequestError('username is required');
|
||||
}
|
||||
});
|
||||
|
||||
UserSchema.pre('remove', async function() {
|
||||
await this.model('TodoList')
|
||||
.find({ user: this._id })
|
||||
.remove()
|
||||
.exec();
|
||||
await this.model('Todo')
|
||||
.find({ user: this._id })
|
||||
.remove()
|
||||
.exec();
|
||||
});
|
||||
|
||||
UserSchema.methods.generateJwt = function() {
|
||||
return jwt.sign({ id: this._id, username: this.username }, secret, {
|
||||
expiresIn: '120d',
|
||||
});
|
||||
};
|
||||
|
||||
UserSchema.methods.toJson = function() {
|
||||
return {
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
};
|
||||
};
|
||||
|
||||
UserSchema.methods.toAuthJson = function() {
|
||||
return {
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
jwt: this.generateJwt(),
|
||||
};
|
||||
};
|
||||
|
||||
mongoose.model('User', UserSchema);
|
||||
5637
package-lock.json
generated
5637
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -5,12 +5,22 @@
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node ./app.js",
|
||||
"start": "node ./src/app.js",
|
||||
"dev": "npx concurrently npm:client npm:server -c 'blue,green'",
|
||||
"client": "cd client && npm start",
|
||||
"server": "npx cross-env NODE_ENV=development npx nodemon --inspect ./app.js",
|
||||
"server": "npx cross-env NODE_ENV=development npx nodemon --inspect ./src/app.js",
|
||||
"test": "npx cross-env NODE_ENV=test jest --runInBand",
|
||||
"heroku-postbuild": "cd client && npm install && npm run build"
|
||||
"test-frontend": "cd client && npm test",
|
||||
"test-all": "npm test && npm run test-frontend",
|
||||
"heroku-postbuild": "cd client && npm install && npm run build",
|
||||
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
|
||||
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"lint-frontend": "cd client && npm run lint",
|
||||
"lint-frontend-fix": "cd client && npm run lint-fix",
|
||||
"lint-all": "npm run lint && npm run lint-frontend",
|
||||
"lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix",
|
||||
"prettier-check": "prettier ./src/ ./client/src/ --check",
|
||||
"prettify": "prettier ./src/ ./client/src/ --write"
|
||||
},
|
||||
"cacheDirectories": [
|
||||
"client/node_modules",
|
||||
@@ -30,36 +40,36 @@
|
||||
"express-jwt": "^6.0.0",
|
||||
"hsts": "^2.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^5.10.9",
|
||||
"mongoose": "^5.12.0",
|
||||
"mongoose-findorcreate": "^3.0.0",
|
||||
"mongoose-unique-validator": "^2.0.3",
|
||||
"morgan": "^1.10.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport-google-oauth": "^2.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^6.0.1"
|
||||
"passport-local-mongoose": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^5.3.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"eslint": "^6.0.0",
|
||||
"eslint-config-airbnb-base": "^14.2.0",
|
||||
"eslint-config-prettier": "^6.12.0",
|
||||
"concurrently": "^6.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^23.20.0",
|
||||
"eslint-plugin-jest": "^24.2.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.21.4",
|
||||
"jest": "^24.0.0",
|
||||
"mongodb-memory-server": "^6.9.2",
|
||||
"nodemon": "^2.0.5",
|
||||
"prettier-eslint": "^11.0.0",
|
||||
"supertest": "^5.0.0"
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"jest": "26.6.0",
|
||||
"mongodb-memory-server": "^6.9.6",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier-eslint": "^12.0.0",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"roots": [
|
||||
"<rootDir>/tests/"
|
||||
"<rootDir>/src/tests/"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
const jwt = require('express-jwt');
|
||||
const { secret } = require('../config');
|
||||
|
||||
module.exports = {
|
||||
required: jwt({ secret, algorithms: ['HS256'] }),
|
||||
optional: jwt({ secret, credentialsRequired: false, algorithms: ['HS256'] }),
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const asyncHelper = require('../asyncHelper');
|
||||
|
||||
router.get(
|
||||
'/google',
|
||||
passport.authenticate('google', {
|
||||
scope: ['https://www.googleapis.com/auth/plus.login'],
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/google/callback',
|
||||
passport.authenticate('google', { session: false, failWithError: true }),
|
||||
asyncHelper(async (req, res) => {
|
||||
res.redirect(`/login?jwt=${req.user.generateJwt()}`);
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,74 +0,0 @@
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const TodoList = mongoose.model('TodoList');
|
||||
|
||||
const asyncHelper = require('../asyncHelper');
|
||||
const { NotFoundError } = require('../errors');
|
||||
|
||||
// index
|
||||
router.get(
|
||||
'/',
|
||||
asyncHelper(async (req, res) => {
|
||||
const lists = await TodoList.find({ user: req.user.id })
|
||||
.populate('todos')
|
||||
.exec();
|
||||
res.json({ success: true, data: lists.map(list => { list.todos.reverse(); return list.toJson() }) });
|
||||
}),
|
||||
);
|
||||
|
||||
// create
|
||||
router.post(
|
||||
'/',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { name } = req.body;
|
||||
const { id } = req.body || mongoose.Types.ObjectId();
|
||||
const newList = new TodoList({ name, user: req.user.id, _id: id });
|
||||
await newList.save();
|
||||
res.json({ success: true, data: newList.toJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
// delete
|
||||
router.delete(
|
||||
'/:listId',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = req.params;
|
||||
const list = await TodoList.findOne({
|
||||
_id: listId,
|
||||
user: req.user.id,
|
||||
}).exec();
|
||||
await list.remove();
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
// update
|
||||
router.patch(
|
||||
'/:listId',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = req.params;
|
||||
const { name } = req.body;
|
||||
const list = await TodoList.findOne({ _id: listId, user: req.user.id });
|
||||
if (!list) {
|
||||
throw new NotFoundError("can't find list");
|
||||
}
|
||||
if (name !== undefined) {
|
||||
list.name = name;
|
||||
}
|
||||
await list.save();
|
||||
res.json({ success: true, data: list.toJson() });
|
||||
}),
|
||||
);
|
||||
router.use(
|
||||
'/:listId/todos',
|
||||
(req, res, next) => {
|
||||
res.locals.listId = req.params.listId;
|
||||
next();
|
||||
},
|
||||
require('./todos'),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,71 +0,0 @@
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const Todo = mongoose.model('Todo');
|
||||
|
||||
const asyncHelper = require('../asyncHelper');
|
||||
|
||||
const { NotFoundError } = require('../errors');
|
||||
// index
|
||||
router.get(
|
||||
'/',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = res.locals || req.body;
|
||||
const todos = listId
|
||||
? await Todo.find({ list: listId, user: req.user.id }).exec()
|
||||
: await Todo.find({ user: req.user.id }).exec();
|
||||
res.json({ success: true, data: todos.reverse().map(todo => todo.toJson()) });
|
||||
}),
|
||||
);
|
||||
|
||||
// create
|
||||
router.post(
|
||||
'/',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = res.locals || req.body;
|
||||
const { text } = req.body;
|
||||
const { id } = req.body || mongoose.Types.ObjectId();
|
||||
const todo = new Todo({ text, list: listId, user: req.user.id, _id: id });
|
||||
await todo.save();
|
||||
res.json({ success: true, data: todo.toJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
// update
|
||||
router.patch(
|
||||
'/:todoId',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { todoId } = req.params;
|
||||
const { text, completed } = req.body;
|
||||
const todo = await Todo.findOne({ _id: todoId, user: req.user.id });
|
||||
if (!todo) {
|
||||
throw new NotFoundError("can't find todo");
|
||||
}
|
||||
if (text !== undefined) {
|
||||
todo.text = text;
|
||||
}
|
||||
if (completed !== undefined) {
|
||||
todo.completed = completed;
|
||||
}
|
||||
await todo.save();
|
||||
res.json({ success: true, data: todo.toJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
// delete
|
||||
router.delete(
|
||||
'/:todoId',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { todoId } = req.params;
|
||||
const todo = await Todo.findOne({ _id: todoId, user: req.user.id }).exec();
|
||||
if (!todo) {
|
||||
throw new NotFoundError(`can't find todo with id ${todoId}`);
|
||||
}
|
||||
await todo.remove();
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,93 +0,0 @@
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const passport = require('passport');
|
||||
|
||||
const User = mongoose.model('User');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const asyncHelper = require('../asyncHelper');
|
||||
const auth = require('./auth');
|
||||
const { googleEnabled } = require('../config').googleOAuth;
|
||||
const googleOAuth = require('./google');
|
||||
|
||||
const { NotFoundError } = require('../errors');
|
||||
|
||||
router.get(
|
||||
'/user',
|
||||
auth.required,
|
||||
asyncHelper(async (req, res) => {
|
||||
const { id } = req.user;
|
||||
const user = await User.findById(id).exec();
|
||||
res.json({ success: true, data: user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
asyncHelper(async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = new User({ username });
|
||||
await user.setPassword(password);
|
||||
await user.save();
|
||||
res.json({ success: true, data: user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/user',
|
||||
auth.required,
|
||||
asyncHelper(async (req, res) => {
|
||||
const { username, password, google } = req.body;
|
||||
const patch = {};
|
||||
if (username !== undefined && username != '') {
|
||||
patch.username = username;
|
||||
}
|
||||
if (google === null) {
|
||||
patch.googleId = null;
|
||||
}
|
||||
let user;
|
||||
if (patch !== {}) {
|
||||
user = await User.findOneAndUpdate(
|
||||
{ _id: req.user.id },
|
||||
{ $set: patch },
|
||||
{ runValidators: true, context: 'query', new: true },
|
||||
).exec();
|
||||
} else {
|
||||
user = await User.findById(req.user.id);
|
||||
}
|
||||
if (!user) {
|
||||
throw new NotFoundError(
|
||||
`can't find user with username ${req.user.username}`,
|
||||
);
|
||||
}
|
||||
if (password !== undefined) {
|
||||
await user.setPassword(password);
|
||||
await user.save();
|
||||
}
|
||||
res.json({ success: true, data: user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/user',
|
||||
auth.required,
|
||||
asyncHelper(async (req, res) => {
|
||||
const user = await User.findById(req.user.id).exec();
|
||||
await user.remove();
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
if (googleEnabled) {
|
||||
router.use('/login', googleOAuth);
|
||||
}
|
||||
router.post(
|
||||
'/login',
|
||||
passport.authenticate('local', { session: false, failWithError: true }),
|
||||
asyncHelper(async (req, res) => {
|
||||
res.json({ success: true, data: req.user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
122
src/app.js
Normal file
122
src/app.js
Normal file
@@ -0,0 +1,122 @@
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const morgan = require("morgan");
|
||||
const cors = require("cors");
|
||||
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");
|
||||
require("./models/Todo");
|
||||
|
||||
const app = express();
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(cors());
|
||||
app.use(compression());
|
||||
process.env.NODE_ENV === "production"
|
||||
? app.use(morgan("combined"))
|
||||
: app.use(morgan("dev"));
|
||||
|
||||
if (process.env.NODE_ENV === "production" && process.env.HSTS === "true") {
|
||||
app.use(redirectToHTTPS([/localhost:(\d{4})/]));
|
||||
app.use(
|
||||
hsts({
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const passport = require("./config/passport");
|
||||
|
||||
app.use(passport.initialize());
|
||||
|
||||
// Addresses, starting with /__, are not cached by service worker
|
||||
// https://github.com/facebook/create-react-app/issues/2237
|
||||
|
||||
app.use("/__/users", require("./routes/users"));
|
||||
|
||||
const auth = require("./routes/auth");
|
||||
|
||||
app.use("/__/lists", auth.required, require("./routes/lists"));
|
||||
app.use("/__/todos", auth.required, require("./routes/todos"));
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === "production" ||
|
||||
process.env.NODE_ENV === "development"
|
||||
) {
|
||||
app.use(express.static(path.join(__dirname, "client/build")));
|
||||
app.use(
|
||||
"*",
|
||||
express.static(path.join(__dirname, "client/build/index.html")),
|
||||
);
|
||||
}
|
||||
|
||||
// 404 route
|
||||
app.use((req, res) => {
|
||||
res.status(404);
|
||||
|
||||
if (req.accepts("html")) {
|
||||
res.send("404");
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.accepts("json")) {
|
||||
res.send({ error: "Not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.type("txt").send("not found");
|
||||
});
|
||||
|
||||
// handle errors
|
||||
app.use((error, req, res, next) => {
|
||||
if (error.status) {
|
||||
res.status(error.status);
|
||||
} 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 (
|
||||
process.env.NODE_ENV === "production" ||
|
||||
process.env.NODE_ENV === "test"
|
||||
) {
|
||||
console.error(error);
|
||||
}
|
||||
next(error);
|
||||
});
|
||||
|
||||
let server;
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
db.connect();
|
||||
server = app.listen(config.app.port, () => {
|
||||
console.log(`Listening on port ${config.app.port}`);
|
||||
console.log("Started!");
|
||||
});
|
||||
} else {
|
||||
server = app;
|
||||
}
|
||||
|
||||
module.exports = server;
|
||||
3
src/asyncHelper.js
Normal file
3
src/asyncHelper.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = (fn) => (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
18
src/config/db.js
Normal file
18
src/config/db.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const mongoose = require("mongoose");
|
||||
const config = require("./");
|
||||
|
||||
async function connect() {
|
||||
await mongoose.connect(config.db.uri, {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
40
src/config/index.js
Normal file
40
src/config/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const env = process.env.NODE_ENV;
|
||||
|
||||
const production = {
|
||||
app: {
|
||||
port: process.env.PORT || 4000,
|
||||
},
|
||||
db: {
|
||||
uri:
|
||||
process.env.DB_URI ||
|
||||
process.env.MONGODB_URI ||
|
||||
"mongodb://localhost/todolist",
|
||||
},
|
||||
googleOAuth: {
|
||||
googleEnabled: process.env.GOOGLE_ENABLED
|
||||
? process.env.GOOGLE_ENABLED.toUpperCase() === "TRUE"
|
||||
: false,
|
||||
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
||||
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
googleCallback: `${process.env.HOST}/__/users/login/google/callback`,
|
||||
},
|
||||
secret: process.env.SECRET,
|
||||
};
|
||||
|
||||
const development = {
|
||||
...production,
|
||||
secret: process.env.SECRET || "devsecret",
|
||||
};
|
||||
|
||||
const test = {
|
||||
...production,
|
||||
secret: process.env.SECRET || "testsecret",
|
||||
};
|
||||
|
||||
const config = {
|
||||
production,
|
||||
development,
|
||||
test,
|
||||
};
|
||||
|
||||
module.exports = config[env] || config.production;
|
||||
31
src/config/passport.js
Normal file
31
src/config/passport.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const passport = require("passport");
|
||||
const mongoose = require("mongoose");
|
||||
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy;
|
||||
const {
|
||||
googleClientId,
|
||||
googleClientSecret,
|
||||
googleCallback,
|
||||
googleEnabled,
|
||||
} = require(".").googleOAuth;
|
||||
|
||||
const User = mongoose.model("User");
|
||||
|
||||
passport.use(User.createStrategy());
|
||||
|
||||
if (googleEnabled) {
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: googleClientId,
|
||||
clientSecret: googleClientSecret,
|
||||
callbackURL: googleCallback,
|
||||
},
|
||||
(accessToken, refreshToken, profile, done) => {
|
||||
User.findOrCreate({ googleId: profile.id }, (err, user) =>
|
||||
done(err, user),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
module.exports = passport;
|
||||
21
src/errors/index.js
Normal file
21
src/errors/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
class NotFoundError extends Error {
|
||||
constructor(text, ...args) {
|
||||
super(...args);
|
||||
Error.captureStackTrace(this, NotFoundError);
|
||||
this.name = "NotFound";
|
||||
this.text = text;
|
||||
this.status = 404;
|
||||
}
|
||||
}
|
||||
|
||||
class BadRequestError extends Error {
|
||||
constructor(text, ...args) {
|
||||
super(...args);
|
||||
Error.captureStackTrace(this, NotFoundError);
|
||||
this.name = "BadRequest";
|
||||
this.text = text;
|
||||
this.status = 400;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { NotFoundError, BadRequestError };
|
||||
50
src/models/Todo.js
Normal file
50
src/models/Todo.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const TodoSchema = Schema({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
minLength: 1,
|
||||
maxLength: 300,
|
||||
trim: true,
|
||||
},
|
||||
list: { type: Schema.Types.ObjectId, ref: "TodoList", required: true },
|
||||
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
|
||||
completed: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
TodoSchema.pre("save", async function () {
|
||||
if (this.isNew) {
|
||||
const user = await this.model("User").findById(this.user);
|
||||
user.todos.push(this._id);
|
||||
await user.save();
|
||||
|
||||
const list = await this.model("TodoList").findById(this.list);
|
||||
list.todos.push(this._id);
|
||||
await list.save();
|
||||
}
|
||||
});
|
||||
|
||||
TodoSchema.pre("remove", async function () {
|
||||
const user = await this.model("User").findById(this.user);
|
||||
user.todos.splice(user.todos.indexOf(this._id), 1);
|
||||
await user.save();
|
||||
|
||||
const list = await this.model("TodoList").findById(this.list);
|
||||
list.todos.splice(list.todos.indexOf(this._id), 1);
|
||||
await list.save();
|
||||
});
|
||||
|
||||
TodoSchema.methods.toJson = function () {
|
||||
return {
|
||||
id: this._id.toString(),
|
||||
text: this.text,
|
||||
list: this.list.toString(),
|
||||
user: this.user.toString(),
|
||||
completed: this.completed,
|
||||
};
|
||||
};
|
||||
|
||||
mongoose.model("Todo", TodoSchema);
|
||||
51
src/models/TodoList.js
Normal file
51
src/models/TodoList.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const TodoListSchema = Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
minLength: 1,
|
||||
maxLength: 100,
|
||||
trim: true,
|
||||
},
|
||||
todos: [{ type: Schema.Types.ObjectId, ref: "Todo" }],
|
||||
user: { type: Schema.Types.ObjectId, ref: "User", required: true },
|
||||
});
|
||||
|
||||
TodoListSchema.pre("save", async function () {
|
||||
if (this.isNew) {
|
||||
const user = await this.model("User").findById(this.user);
|
||||
user.lists.push(this._id);
|
||||
await user.save();
|
||||
}
|
||||
});
|
||||
|
||||
TodoListSchema.pre("remove", async function () {
|
||||
const user = await this.model("User").findById(this.user);
|
||||
user.lists.splice(user.lists.indexOf(this._id), 1);
|
||||
|
||||
// removing todos in parallel can cause VersionError
|
||||
// so we remove todos from user
|
||||
const todos = await this.model("Todo").find({ list: this._id }).exec();
|
||||
const ids = todos.map((todo) => todo._id);
|
||||
user.todos = user.todos.filter((todo) => ids.includes(todo._id));
|
||||
await user.save();
|
||||
// and remove them from db
|
||||
await this.model("Todo").find({ list: this._id }).remove().exec();
|
||||
});
|
||||
|
||||
TodoListSchema.methods.toJson = function () {
|
||||
const todos = this.populated("todos")
|
||||
? this.todos.map((todo) => todo.toJson())
|
||||
: this.todos;
|
||||
return {
|
||||
id: this._id.toString(),
|
||||
user: this.user.toString(),
|
||||
name: this.name,
|
||||
todos,
|
||||
};
|
||||
};
|
||||
|
||||
mongoose.model("TodoList", TodoListSchema);
|
||||
70
src/models/User.js
Normal file
70
src/models/User.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const mongoose = require("mongoose");
|
||||
const passportLocalMongoose = require("passport-local-mongoose");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const uniqueValidator = require("mongoose-unique-validator");
|
||||
const findOrCreate = require("mongoose-findorcreate");
|
||||
const { BadRequestError } = require("../errors");
|
||||
|
||||
const { secret } = require("../config");
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const UserSchema = Schema({
|
||||
username: {
|
||||
type: String,
|
||||
unique: true,
|
||||
validate: /^\S*$/,
|
||||
minLength: 3,
|
||||
maxLength: 50,
|
||||
trim: true,
|
||||
sparse: true,
|
||||
},
|
||||
googleId: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true,
|
||||
},
|
||||
lists: [{ type: Schema.Types.ObjectId, ref: "TodoList" }],
|
||||
todos: [{ type: Schema.Types.ObjectId, ref: "Todo" }],
|
||||
});
|
||||
|
||||
UserSchema.plugin(passportLocalMongoose, {
|
||||
limitAttempts: true,
|
||||
maxAttempts: 20,
|
||||
});
|
||||
UserSchema.plugin(uniqueValidator);
|
||||
UserSchema.plugin(findOrCreate);
|
||||
|
||||
UserSchema.pre("validate", async function () {
|
||||
if (!this.username && !this.googleId) {
|
||||
throw new BadRequestError("username is required");
|
||||
}
|
||||
});
|
||||
|
||||
UserSchema.pre("remove", async function () {
|
||||
await this.model("TodoList").find({ user: this._id }).remove().exec();
|
||||
await this.model("Todo").find({ user: this._id }).remove().exec();
|
||||
});
|
||||
|
||||
UserSchema.methods.generateJwt = function () {
|
||||
return jwt.sign({ id: this._id, username: this.username }, secret, {
|
||||
expiresIn: "120d",
|
||||
});
|
||||
};
|
||||
|
||||
UserSchema.methods.toJson = function () {
|
||||
return {
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
};
|
||||
};
|
||||
|
||||
UserSchema.methods.toAuthJson = function () {
|
||||
return {
|
||||
id: this._id,
|
||||
username: this.username,
|
||||
jwt: this.generateJwt(),
|
||||
};
|
||||
};
|
||||
|
||||
mongoose.model("User", UserSchema);
|
||||
11
src/routes/auth.js
Normal file
11
src/routes/auth.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const jwt = require("express-jwt");
|
||||
const { secret } = require("../config");
|
||||
|
||||
module.exports = {
|
||||
required: jwt({ secret, algorithms: ["HS256"] }),
|
||||
optional: jwt({
|
||||
secret,
|
||||
credentialsRequired: false,
|
||||
algorithms: ["HS256"],
|
||||
}),
|
||||
};
|
||||
23
src/routes/google.js
Normal file
23
src/routes/google.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const express = require("express");
|
||||
const passport = require("passport");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const asyncHelper = require("../asyncHelper");
|
||||
|
||||
router.get(
|
||||
"/google",
|
||||
passport.authenticate("google", {
|
||||
scope: ["https://www.googleapis.com/auth/plus.login"],
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/google/callback",
|
||||
passport.authenticate("google", { session: false, failWithError: true }),
|
||||
asyncHelper(async (req, res) => {
|
||||
res.redirect(`/login?jwt=${req.user.generateJwt()}`);
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
80
src/routes/lists.js
Normal file
80
src/routes/lists.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const TodoList = mongoose.model("TodoList");
|
||||
|
||||
const asyncHelper = require("../asyncHelper");
|
||||
const { NotFoundError } = require("../errors");
|
||||
|
||||
// index
|
||||
router.get(
|
||||
"/",
|
||||
asyncHelper(async (req, res) => {
|
||||
const lists = await TodoList.find({ user: req.user.id })
|
||||
.populate("todos")
|
||||
.exec();
|
||||
res.json({
|
||||
success: true,
|
||||
data: lists.map((list) => {
|
||||
list.todos.reverse();
|
||||
return list.toJson();
|
||||
}),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// create
|
||||
router.post(
|
||||
"/",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { name } = req.body;
|
||||
const { id } = req.body || mongoose.Types.ObjectId();
|
||||
const newList = new TodoList({ name, user: req.user.id, _id: id });
|
||||
await newList.save();
|
||||
res.json({ success: true, data: newList.toJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
// delete
|
||||
router.delete(
|
||||
"/:listId",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = req.params;
|
||||
const list = await TodoList.findOne({
|
||||
_id: listId,
|
||||
user: req.user.id,
|
||||
}).exec();
|
||||
await list.remove();
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
// update
|
||||
router.patch(
|
||||
"/:listId",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = req.params;
|
||||
const { name } = req.body;
|
||||
const list = await TodoList.findOne({ _id: listId, user: req.user.id });
|
||||
if (!list) {
|
||||
throw new NotFoundError("can't find list");
|
||||
}
|
||||
if (name !== undefined) {
|
||||
list.name = name;
|
||||
}
|
||||
await list.save();
|
||||
res.json({ success: true, data: list.toJson() });
|
||||
}),
|
||||
);
|
||||
router.use(
|
||||
"/:listId/todos",
|
||||
(req, res, next) => {
|
||||
res.locals.listId = req.params.listId;
|
||||
next();
|
||||
},
|
||||
require("./todos"),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
82
src/routes/todos.js
Normal file
82
src/routes/todos.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const Todo = mongoose.model("Todo");
|
||||
|
||||
const asyncHelper = require("../asyncHelper");
|
||||
|
||||
const { NotFoundError } = require("../errors");
|
||||
// index
|
||||
router.get(
|
||||
"/",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = res.locals || req.body;
|
||||
const todos = listId
|
||||
? await Todo.find({ list: listId, user: req.user.id }).exec()
|
||||
: await Todo.find({ user: req.user.id }).exec();
|
||||
res.json({
|
||||
success: true,
|
||||
data: todos.reverse().map((todo) => todo.toJson()),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// create
|
||||
router.post(
|
||||
"/",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { listId } = res.locals || req.body;
|
||||
const { text } = req.body;
|
||||
const { id } = req.body || mongoose.Types.ObjectId();
|
||||
const todo = new Todo({
|
||||
text,
|
||||
list: listId,
|
||||
user: req.user.id,
|
||||
_id: id,
|
||||
});
|
||||
await todo.save();
|
||||
res.json({ success: true, data: todo.toJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
// update
|
||||
router.patch(
|
||||
"/:todoId",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { todoId } = req.params;
|
||||
const { text, completed } = req.body;
|
||||
const todo = await Todo.findOne({ _id: todoId, user: req.user.id });
|
||||
if (!todo) {
|
||||
throw new NotFoundError("can't find todo");
|
||||
}
|
||||
if (text !== undefined) {
|
||||
todo.text = text;
|
||||
}
|
||||
if (completed !== undefined) {
|
||||
todo.completed = completed;
|
||||
}
|
||||
await todo.save();
|
||||
res.json({ success: true, data: todo.toJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
// delete
|
||||
router.delete(
|
||||
"/:todoId",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { todoId } = req.params;
|
||||
const todo = await Todo.findOne({
|
||||
_id: todoId,
|
||||
user: req.user.id,
|
||||
}).exec();
|
||||
if (!todo) {
|
||||
throw new NotFoundError(`can't find todo with id ${todoId}`);
|
||||
}
|
||||
await todo.remove();
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
93
src/routes/users.js
Normal file
93
src/routes/users.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
const passport = require("passport");
|
||||
|
||||
const User = mongoose.model("User");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const asyncHelper = require("../asyncHelper");
|
||||
const auth = require("./auth");
|
||||
const { googleEnabled } = require("../config").googleOAuth;
|
||||
const googleOAuth = require("./google");
|
||||
|
||||
const { NotFoundError } = require("../errors");
|
||||
|
||||
router.get(
|
||||
"/user",
|
||||
auth.required,
|
||||
asyncHelper(async (req, res) => {
|
||||
const { id } = req.user;
|
||||
const user = await User.findById(id).exec();
|
||||
res.json({ success: true, data: user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
asyncHelper(async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = new User({ username });
|
||||
await user.setPassword(password);
|
||||
await user.save();
|
||||
res.json({ success: true, data: user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/user",
|
||||
auth.required,
|
||||
asyncHelper(async (req, res) => {
|
||||
const { username, password, google } = req.body;
|
||||
const patch = {};
|
||||
if (username !== undefined && username != "") {
|
||||
patch.username = username;
|
||||
}
|
||||
if (google === null) {
|
||||
patch.googleId = null;
|
||||
}
|
||||
let user;
|
||||
if (patch !== {}) {
|
||||
user = await User.findOneAndUpdate(
|
||||
{ _id: req.user.id },
|
||||
{ $set: patch },
|
||||
{ runValidators: true, context: "query", new: true },
|
||||
).exec();
|
||||
} else {
|
||||
user = await User.findById(req.user.id);
|
||||
}
|
||||
if (!user) {
|
||||
throw new NotFoundError(
|
||||
`can't find user with username ${req.user.username}`,
|
||||
);
|
||||
}
|
||||
if (password !== undefined) {
|
||||
await user.setPassword(password);
|
||||
await user.save();
|
||||
}
|
||||
res.json({ success: true, data: user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/user",
|
||||
auth.required,
|
||||
asyncHelper(async (req, res) => {
|
||||
const user = await User.findById(req.user.id).exec();
|
||||
await user.remove();
|
||||
res.json({ success: true });
|
||||
}),
|
||||
);
|
||||
|
||||
if (googleEnabled) {
|
||||
router.use("/login", googleOAuth);
|
||||
}
|
||||
router.post(
|
||||
"/login",
|
||||
passport.authenticate("local", { session: false, failWithError: true }),
|
||||
asyncHelper(async (req, res) => {
|
||||
res.json({ success: true, data: req.user.toAuthJson() });
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
5
src/tests/.eslintrc.json
Normal file
5
src/tests/.eslintrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"node/no-unpublished-require": "off"
|
||||
}
|
||||
}
|
||||
165
src/tests/integration/lists.test.js
Normal file
165
src/tests/integration/lists.test.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const request = require("supertest");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
require("../../models/Todo");
|
||||
require("../../models/TodoList");
|
||||
require("../../models/User");
|
||||
|
||||
const Todo = mongoose.model("Todo");
|
||||
const TodoList = mongoose.model("TodoList");
|
||||
const User = mongoose.model("User");
|
||||
|
||||
jest.setTimeout(60000);
|
||||
const MongoDBMemoryServer = require("mongodb-memory-server").default;
|
||||
const server = require("../../app.js");
|
||||
const { seed, clean, mongodbMemoryServerConfig } = require("./utils");
|
||||
|
||||
let user;
|
||||
let token;
|
||||
let list;
|
||||
let todo;
|
||||
let mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = new MongoDBMemoryServer(mongodbMemoryServerConfig);
|
||||
const mongoUri = await mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
({ user, token, list, todo } = await seed());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clean();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
describe("test lists", () => {
|
||||
test("should index lists", async () => {
|
||||
const response = await request(server)
|
||||
.get("/__/lists")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data[0].name).toEqual("List1");
|
||||
});
|
||||
test("should not index lists without authentication", async () => {
|
||||
await request(server)
|
||||
.get("/__/lists")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
});
|
||||
test("should create list", async () => {
|
||||
const response = await request(server)
|
||||
.post("/__/lists")
|
||||
.send({
|
||||
name: "List2",
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: "List2" })).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists.map((l) => String(l))).toContain(
|
||||
response.body.data.id,
|
||||
);
|
||||
});
|
||||
test("should create list with custom id", async () => {
|
||||
const id = mongoose.Types.ObjectId();
|
||||
const response = await request(server)
|
||||
.post("/__/lists")
|
||||
.send({
|
||||
name: "List2",
|
||||
id,
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: "List2", _id: id })).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists.map((l) => String(l))).toContain(
|
||||
response.body.data.id,
|
||||
);
|
||||
});
|
||||
|
||||
test("should not create list without authentication", async () => {
|
||||
await request(server)
|
||||
.post("/__/lists")
|
||||
.send({
|
||||
name: "List2",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
});
|
||||
test("should update list", async () => {
|
||||
const response = await request(server)
|
||||
.patch(`/__/lists/${list._id}`)
|
||||
.send({
|
||||
name: "List2",
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: "List2" })).toBeTruthy();
|
||||
});
|
||||
test("should not update list without authentication", async () => {
|
||||
await request(server)
|
||||
.patch(`/__/lists/${list._id}`)
|
||||
.send({
|
||||
name: "List2",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
expect(await TodoList.findOne({ name: "List2" })).toBeFalsy();
|
||||
});
|
||||
test("should remove list", async () => {
|
||||
const response = await request(server)
|
||||
.delete(`/__/lists/${list._id}`)
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: "List1" }).exec()).toBeFalsy();
|
||||
expect(await Todo.findOne({ text: "Todo1" }).exec()).toBeFalsy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists).not.toContain(list._id);
|
||||
expect(freshUser.todos).not.toContain(todo._id);
|
||||
});
|
||||
test("should not remove list without authentication", async () => {
|
||||
await request(server)
|
||||
.delete(`/__/lists/${list._id}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
expect(await TodoList.findOne({ name: "List1" }).exec()).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: "Todo1" }).exec()).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists.map((l) => String(l))).toContain(
|
||||
String(list._id),
|
||||
);
|
||||
expect(freshUser.todos.map((t) => String(t))).toContain(
|
||||
String(todo._id),
|
||||
);
|
||||
});
|
||||
});
|
||||
30
src/tests/integration/root.test.js
Normal file
30
src/tests/integration/root.test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const server = require("../../app.js");
|
||||
|
||||
const request = require("supertest");
|
||||
|
||||
describe("test not found", () => {
|
||||
test("respond not found with json", async () => {
|
||||
const response = await request(server)
|
||||
.get("/")
|
||||
.set("Accept", "application/json")
|
||||
.expect(404)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body).toEqual({ error: "Not found" });
|
||||
});
|
||||
test("respond not found with html", async () => {
|
||||
const response = await request(server)
|
||||
.get("/")
|
||||
.set("Accept", "text/html")
|
||||
.expect(404)
|
||||
.expect("Content-Type", "text/html; charset=utf-8");
|
||||
expect(response.text).toEqual("404");
|
||||
});
|
||||
test("respond not found with plain text", async () => {
|
||||
const response = await request(server)
|
||||
.get("/")
|
||||
.set("Accept", "text/plain")
|
||||
.expect(404)
|
||||
.expect("Content-Type", "text/plain; charset=utf-8");
|
||||
expect(response.text).toEqual("not found");
|
||||
});
|
||||
});
|
||||
181
src/tests/integration/todos.test.js
Normal file
181
src/tests/integration/todos.test.js
Normal file
@@ -0,0 +1,181 @@
|
||||
const request = require("supertest");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
require("../../models/Todo");
|
||||
require("../../models/TodoList");
|
||||
require("../../models/User");
|
||||
|
||||
const Todo = mongoose.model("Todo");
|
||||
const TodoList = mongoose.model("TodoList");
|
||||
const User = mongoose.model("User");
|
||||
|
||||
jest.setTimeout(60000);
|
||||
const MongoDBMemoryServer = require("mongodb-memory-server").default;
|
||||
const server = require("../../app.js");
|
||||
const { seed, clean, mongodbMemoryServerConfig } = require("./utils");
|
||||
|
||||
let user;
|
||||
let token;
|
||||
let list;
|
||||
let todo;
|
||||
let mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = new MongoDBMemoryServer(mongodbMemoryServerConfig);
|
||||
const mongoUri = await mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
({ user, token, list, todo } = await seed());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clean();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
describe("test todos", () => {
|
||||
test("should index todos", async () => {
|
||||
const response = await request(server)
|
||||
.get(`/__/lists/${list._id}/todos`)
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data[0].text).toEqual("Todo1");
|
||||
});
|
||||
test("should index all todos", async () => {
|
||||
const response = await request(server)
|
||||
.get(`/__/todos`)
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data[0].text).toEqual("Todo1");
|
||||
});
|
||||
test("should not index todos without authentication", async () => {
|
||||
await request(server)
|
||||
.get(`/__/lists/${list._id}/todos`)
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
});
|
||||
test("should create todo", async () => {
|
||||
const response = await request(server)
|
||||
.post(`/__/lists/${list._id}/todos`)
|
||||
.send({
|
||||
text: "Todo2",
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(
|
||||
await Todo.findOne({ text: "Todo2", list: list._id }),
|
||||
).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.todos.map((t) => String(t))).toContain(
|
||||
response.body.data.id,
|
||||
);
|
||||
const freshList = await TodoList.findById(list.id).exec();
|
||||
expect(freshList.todos.map((t) => String(t))).toContain(
|
||||
response.body.data.id,
|
||||
);
|
||||
});
|
||||
test("should create todo with custom id", async () => {
|
||||
const id = mongoose.Types.ObjectId();
|
||||
const response = await request(server)
|
||||
.post(`/__/lists/${list._id}/todos`)
|
||||
.send({
|
||||
text: "Todo2",
|
||||
id,
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(
|
||||
await Todo.findOne({ text: "Todo2", list: list._id, _id: id }),
|
||||
).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.todos.map((t) => String(t))).toContain(
|
||||
response.body.data.id,
|
||||
);
|
||||
const freshList = await TodoList.findById(list.id).exec();
|
||||
expect(freshList.todos.map((t) => String(t))).toContain(
|
||||
response.body.data.id,
|
||||
);
|
||||
});
|
||||
test("should not create todo without authentication", async () => {
|
||||
await request(server)
|
||||
.post(`/__/lists/${list._id}/todos`)
|
||||
.send({
|
||||
text: "Todo1",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
});
|
||||
test("should update todo", async () => {
|
||||
const response = await request(server)
|
||||
.patch(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.send({
|
||||
text: "Todo2",
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: "Todo2" })).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: "Todo1" })).toBeFalsy();
|
||||
});
|
||||
test("should not update todo without authentication", async () => {
|
||||
await request(server)
|
||||
.patch(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.send({
|
||||
text: "Todo2",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
expect(await Todo.findOne({ text: "Todo1" })).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: "Todo2" })).toBeFalsy();
|
||||
});
|
||||
test("should remove todo", async () => {
|
||||
const response = await request(server)
|
||||
.delete(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: "Todo1" }).exec()).toBeFalsy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.todos).not.toContain(todo.id);
|
||||
const freshList = await TodoList.findById(list.id).exec();
|
||||
expect(freshList.todos).not.toContain(todo.id);
|
||||
});
|
||||
test("should not remove todo without authentication", async () => {
|
||||
await request(server)
|
||||
.delete(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
expect(await Todo.findOne({ text: "Todo1" }).exec()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
189
src/tests/integration/users.test.js
Normal file
189
src/tests/integration/users.test.js
Normal file
@@ -0,0 +1,189 @@
|
||||
const request = require("supertest");
|
||||
const mongoose = require("mongoose");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
require("../../models/Todo");
|
||||
require("../../models/TodoList");
|
||||
require("../../models/User");
|
||||
|
||||
const Todo = mongoose.model("Todo");
|
||||
const TodoList = mongoose.model("TodoList");
|
||||
const User = mongoose.model("User");
|
||||
|
||||
jest.setTimeout(60000);
|
||||
const MongoDBMemoryServer = require("mongodb-memory-server").default;
|
||||
const server = require("../../app.js");
|
||||
const { seed, clean, mongodbMemoryServerConfig } = require("./utils");
|
||||
const { secret } = require("../../config");
|
||||
|
||||
let token;
|
||||
let user;
|
||||
let mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = new MongoDBMemoryServer(mongodbMemoryServerConfig);
|
||||
const mongoUri = await mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
({ token, user } = await seed());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clean();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
describe("test users", () => {
|
||||
test("should get user", async () => {
|
||||
const response = await request(server)
|
||||
.get("/__/users/user")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data.id).toBe(user._id.toString());
|
||||
expect(response.body.data.username).toBe(user.username);
|
||||
});
|
||||
test("should create user", async () => {
|
||||
const response = await request(server)
|
||||
.post("/__/users")
|
||||
.send({
|
||||
username: "User2",
|
||||
password: "password2",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
|
||||
expect(tokenDecoded.username).toEqual("User2");
|
||||
const userAuth = await User.authenticate()("User2", "password2");
|
||||
expect(userAuth.user).toBeTruthy();
|
||||
});
|
||||
test("should not create user with no username", async () => {
|
||||
const response = await request(server)
|
||||
.post("/__/users")
|
||||
.send({
|
||||
username: "",
|
||||
password: "password2",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(400)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeFalsy();
|
||||
});
|
||||
test("should not create user with no password", async () => {
|
||||
const response = await request(server)
|
||||
.post("/__/users")
|
||||
.send({
|
||||
username: "User",
|
||||
password: "",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(400)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeFalsy();
|
||||
});
|
||||
test("should login user", async () => {
|
||||
const response = await request(server)
|
||||
.post("/__/users/login")
|
||||
.send({
|
||||
username: "User1",
|
||||
password: "password1",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
|
||||
expect(tokenDecoded.username).toEqual("User1");
|
||||
});
|
||||
test("should not login user with no name", async () => {
|
||||
await request(server)
|
||||
.post("/__/users/login")
|
||||
.send({
|
||||
username: "",
|
||||
password: "notpassword",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(400);
|
||||
});
|
||||
test("should not login user with wrong password", async () => {
|
||||
await request(server)
|
||||
.post("/__/users/login")
|
||||
.send({
|
||||
username: "User",
|
||||
password: "notpassword",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
});
|
||||
test("should update user", async () => {
|
||||
const response = await request(server)
|
||||
.patch("/__/users/user")
|
||||
.send({
|
||||
username: "User2",
|
||||
password: "password2",
|
||||
})
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
|
||||
expect(tokenDecoded.username).toEqual("User2");
|
||||
const userAuth = await User.authenticate()("User2", "password2");
|
||||
expect(userAuth.user).toBeTruthy();
|
||||
});
|
||||
test("should not update user without authentication", async () => {
|
||||
const response = await request(server)
|
||||
.patch("/__/users/user")
|
||||
.send({
|
||||
username: "User2",
|
||||
password: "password2",
|
||||
})
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
expect(response.body.success).toBeFalsy();
|
||||
});
|
||||
test("should not delete user without authentication", async () => {
|
||||
const response = await request(server)
|
||||
.delete("/__/users/user")
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(401);
|
||||
expect(response.body.success).toBeFalsy();
|
||||
expect(await User.findOne({ username: "User1" }).exec()).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: "List1" }).exec()).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: "Todo1" })).toBeTruthy();
|
||||
});
|
||||
test("should delete user", async () => {
|
||||
const response = await request(server)
|
||||
.delete("/__/users/user")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.set("Accept", "application/json")
|
||||
.expect(200)
|
||||
.expect("Content-Type", "application/json; charset=utf-8");
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await User.findOne({ username: "User1" }).exec()).toBeFalsy();
|
||||
expect(await TodoList.findOne({ name: "List1" }).exec()).toBeFalsy();
|
||||
expect(await Todo.findOne({ text: "Todo1" }).exec()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
46
src/tests/integration/utils.js
Normal file
46
src/tests/integration/utils.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
require("../../models/Todo");
|
||||
require("../../models/TodoList");
|
||||
require("../../models/User");
|
||||
|
||||
const User = mongoose.model("User");
|
||||
const Todo = mongoose.model("Todo");
|
||||
const TodoList = mongoose.model("TodoList");
|
||||
|
||||
async function seed() {
|
||||
const user = new User({ username: "User1" });
|
||||
await user.setPassword("password1");
|
||||
await user.save();
|
||||
const token = user.generateJwt();
|
||||
|
||||
const list = new TodoList({ name: "List1", user: user._id });
|
||||
const todo = new Todo({ text: "Todo1", list: list._id, user: user._id });
|
||||
|
||||
await list.save();
|
||||
await todo.save();
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
list,
|
||||
todo,
|
||||
};
|
||||
}
|
||||
|
||||
async function clean() {
|
||||
await TodoList.remove({}).exec();
|
||||
await Todo.remove({}).exec();
|
||||
await User.remove({}).exec();
|
||||
}
|
||||
|
||||
const mongodbMemoryServerConfig = {
|
||||
binary: {
|
||||
version: "4.0.14",
|
||||
},
|
||||
instance: {
|
||||
args: ["--enableMajorityReadConcern=false"],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { seed, clean, mongodbMemoryServerConfig };
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"node/no-unpublished-require": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
require('../../models/Todo');
|
||||
require('../../models/TodoList');
|
||||
require('../../models/User');
|
||||
|
||||
const Todo = mongoose.model('Todo');
|
||||
const TodoList = mongoose.model('TodoList');
|
||||
const User = mongoose.model('User');
|
||||
|
||||
jest.setTimeout(60000);
|
||||
const MongoDBMemoryServer = require('mongodb-memory-server').default;
|
||||
const server = require('../../app.js');
|
||||
const { seed, clean, mongodbMemoryServerConfig } = require('./utils');
|
||||
|
||||
let user;
|
||||
let token;
|
||||
let list;
|
||||
let todo;
|
||||
let mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = new MongoDBMemoryServer(mongodbMemoryServerConfig);
|
||||
const mongoUri = await mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
({ user, token, list, todo } = await seed());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clean();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
describe('test lists', () => {
|
||||
test('should index lists', async () => {
|
||||
const response = await request(server)
|
||||
.get('/__/lists')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data[0].name).toEqual('List1');
|
||||
});
|
||||
test('should not index lists without authentication', async () => {
|
||||
await request(server)
|
||||
.get('/__/lists')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
});
|
||||
test('should create list', async () => {
|
||||
const response = await request(server)
|
||||
.post('/__/lists')
|
||||
.send({
|
||||
name: 'List2',
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: 'List2' })).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists.map(l => String(l))).toContain(response.body.data.id);
|
||||
});
|
||||
test('should create list with custom id', async () => {
|
||||
const id = mongoose.Types.ObjectId();
|
||||
const response = await request(server)
|
||||
.post('/__/lists')
|
||||
.send({
|
||||
name: 'List2',
|
||||
id,
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: 'List2', _id: id })).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists.map(l => String(l))).toContain(response.body.data.id);
|
||||
});
|
||||
|
||||
test('should not create list without authentication', async () => {
|
||||
await request(server)
|
||||
.post('/__/lists')
|
||||
.send({
|
||||
name: 'List2',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
});
|
||||
test('should update list', async () => {
|
||||
const response = await request(server)
|
||||
.patch(`/__/lists/${list._id}`)
|
||||
.send({
|
||||
name: 'List2',
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: 'List2' })).toBeTruthy();
|
||||
});
|
||||
test('should not update list without authentication', async () => {
|
||||
await request(server)
|
||||
.patch(`/__/lists/${list._id}`)
|
||||
.send({
|
||||
name: 'List2',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
expect(await TodoList.findOne({ name: 'List2' })).toBeFalsy();
|
||||
});
|
||||
test('should remove list', async () => {
|
||||
const response = await request(server)
|
||||
.delete(`/__/lists/${list._id}`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeFalsy();
|
||||
expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeFalsy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists).not.toContain(list._id);
|
||||
expect(freshUser.todos).not.toContain(todo._id);
|
||||
});
|
||||
test('should not remove list without authentication', async () => {
|
||||
await request(server)
|
||||
.delete(`/__/lists/${list._id}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.lists.map(l => String(l))).toContain(String(list._id));
|
||||
expect(freshUser.todos.map(t => String(t))).toContain(String(todo._id));
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
const server = require('../../app.js');
|
||||
|
||||
const request = require('supertest');
|
||||
|
||||
describe('test not found', () => {
|
||||
test('respond not found with json', async () => {
|
||||
const response = await request(server)
|
||||
.get('/')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(404)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body).toEqual({ error: 'Not found' });
|
||||
});
|
||||
test('respond not found with html', async () => {
|
||||
const response = await request(server)
|
||||
.get('/')
|
||||
.set('Accept', 'text/html')
|
||||
.expect(404)
|
||||
.expect('Content-Type', 'text/html; charset=utf-8');
|
||||
expect(response.text).toEqual('404');
|
||||
});
|
||||
test('respond not found with plain text', async () => {
|
||||
const response = await request(server)
|
||||
.get('/')
|
||||
.set('Accept', 'text/plain')
|
||||
.expect(404)
|
||||
.expect('Content-Type', 'text/plain; charset=utf-8');
|
||||
expect(response.text).toEqual('not found');
|
||||
});
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
require('../../models/Todo');
|
||||
require('../../models/TodoList');
|
||||
require('../../models/User');
|
||||
|
||||
const Todo = mongoose.model('Todo');
|
||||
const TodoList = mongoose.model('TodoList');
|
||||
const User = mongoose.model('User');
|
||||
|
||||
jest.setTimeout(60000);
|
||||
const MongoDBMemoryServer = require('mongodb-memory-server').default;
|
||||
const server = require('../../app.js');
|
||||
const { seed, clean, mongodbMemoryServerConfig } = require('./utils');
|
||||
|
||||
let user;
|
||||
let token;
|
||||
let list;
|
||||
let todo;
|
||||
let mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = new MongoDBMemoryServer(mongodbMemoryServerConfig);
|
||||
const mongoUri = await mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
({ user, token, list, todo } = await seed());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clean();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
describe('test todos', () => {
|
||||
test('should index todos', async () => {
|
||||
const response = await request(server)
|
||||
.get(`/__/lists/${list._id}/todos`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data[0].text).toEqual('Todo1');
|
||||
});
|
||||
test('should index all todos', async () => {
|
||||
const response = await request(server)
|
||||
.get(`/__/todos`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data[0].text).toEqual('Todo1');
|
||||
});
|
||||
test('should not index todos without authentication', async () => {
|
||||
await request(server)
|
||||
.get(`/__/lists/${list._id}/todos`)
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
});
|
||||
test('should create todo', async () => {
|
||||
const response = await request(server)
|
||||
.post(`/__/lists/${list._id}/todos`)
|
||||
.send({
|
||||
text: 'Todo2',
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo2', list: list._id })).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.todos.map(t => String(t))).toContain(response.body.data.id);
|
||||
const freshList = await TodoList.findById(list.id).exec();
|
||||
expect(freshList.todos.map(t => String(t))).toContain(response.body.data.id);
|
||||
});
|
||||
test('should create todo with custom id', async () => {
|
||||
const id = mongoose.Types.ObjectId();
|
||||
const response = await request(server)
|
||||
.post(`/__/lists/${list._id}/todos`)
|
||||
.send({
|
||||
text: 'Todo2',
|
||||
id,
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(
|
||||
await Todo.findOne({ text: 'Todo2', list: list._id, _id: id }),
|
||||
).toBeTruthy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.todos.map(t => String(t))).toContain(response.body.data.id);
|
||||
const freshList = await TodoList.findById(list.id).exec();
|
||||
expect(freshList.todos.map(t => String(t))).toContain(response.body.data.id);
|
||||
});
|
||||
test('should not create todo without authentication', async () => {
|
||||
await request(server)
|
||||
.post(`/__/lists/${list._id}/todos`)
|
||||
.send({
|
||||
text: 'Todo1',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
});
|
||||
test('should update todo', async () => {
|
||||
const response = await request(server)
|
||||
.patch(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.send({
|
||||
text: 'Todo2',
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo2' })).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo1' })).toBeFalsy();
|
||||
});
|
||||
test('should not update todo without authentication', async () => {
|
||||
await request(server)
|
||||
.patch(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.send({
|
||||
text: 'Todo2',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
expect(await Todo.findOne({ text: 'Todo1' })).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo2' })).toBeFalsy();
|
||||
});
|
||||
test('should remove todo', async () => {
|
||||
const response = await request(server)
|
||||
.delete(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeFalsy();
|
||||
const freshUser = await User.findById(user.id).exec();
|
||||
expect(freshUser.todos).not.toContain(todo.id);
|
||||
const freshList = await TodoList.findById(list.id).exec();
|
||||
expect(freshList.todos).not.toContain(todo.id);
|
||||
});
|
||||
test('should not remove todo without authentication', async () => {
|
||||
await request(server)
|
||||
.delete(`/__/lists/${list._id}/todos/${todo._id}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,189 +0,0 @@
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
require('../../models/Todo');
|
||||
require('../../models/TodoList');
|
||||
require('../../models/User');
|
||||
|
||||
const Todo = mongoose.model('Todo');
|
||||
const TodoList = mongoose.model('TodoList');
|
||||
const User = mongoose.model('User');
|
||||
|
||||
jest.setTimeout(60000);
|
||||
const MongoDBMemoryServer = require('mongodb-memory-server').default;
|
||||
const server = require('../../app.js');
|
||||
const { seed, clean, mongodbMemoryServerConfig } = require('./utils');
|
||||
const { secret } = require('../../config');
|
||||
|
||||
let token;
|
||||
let user;
|
||||
let mongoServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
mongoServer = new MongoDBMemoryServer(mongodbMemoryServerConfig);
|
||||
const mongoUri = await mongoServer.getUri();
|
||||
await mongoose.connect(mongoUri);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
({ token, user } = await seed());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clean();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await mongoose.disconnect();
|
||||
await mongoServer.stop();
|
||||
});
|
||||
|
||||
describe('test users', () => {
|
||||
test('should get user', async () => {
|
||||
const response = await request(server)
|
||||
.get('/__/users/user')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(response.body.data.id).toBe(user._id.toString());
|
||||
expect(response.body.data.username).toBe(user.username);
|
||||
});
|
||||
test('should create user', async () => {
|
||||
const response = await request(server)
|
||||
.post('/__/users')
|
||||
.send({
|
||||
username: 'User2',
|
||||
password: 'password2',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
|
||||
expect(tokenDecoded.username).toEqual('User2');
|
||||
const userAuth = await User.authenticate()('User2', 'password2');
|
||||
expect(userAuth.user).toBeTruthy();
|
||||
});
|
||||
test('should not create user with no username', async () => {
|
||||
const response = await request(server)
|
||||
.post('/__/users')
|
||||
.send({
|
||||
username: '',
|
||||
password: 'password2',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(400)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeFalsy();
|
||||
});
|
||||
test('should not create user with no password', async () => {
|
||||
const response = await request(server)
|
||||
.post('/__/users')
|
||||
.send({
|
||||
username: 'User',
|
||||
password: '',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(400)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeFalsy();
|
||||
});
|
||||
test('should login user', async () => {
|
||||
const response = await request(server)
|
||||
.post('/__/users/login')
|
||||
.send({
|
||||
username: 'User1',
|
||||
password: 'password1',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
|
||||
expect(tokenDecoded.username).toEqual('User1');
|
||||
});
|
||||
test('should not login user with no name', async () => {
|
||||
await request(server)
|
||||
.post('/__/users/login')
|
||||
.send({
|
||||
username: '',
|
||||
password: 'notpassword',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(400);
|
||||
});
|
||||
test('should not login user with wrong password', async () => {
|
||||
await request(server)
|
||||
.post('/__/users/login')
|
||||
.send({
|
||||
username: 'User',
|
||||
password: 'notpassword',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
});
|
||||
test('should update user', async () => {
|
||||
const response = await request(server)
|
||||
.patch('/__/users/user')
|
||||
.send({
|
||||
username: 'User2',
|
||||
password: 'password2',
|
||||
})
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
|
||||
expect(tokenDecoded.username).toEqual('User2');
|
||||
const userAuth = await User.authenticate()('User2', 'password2');
|
||||
expect(userAuth.user).toBeTruthy();
|
||||
});
|
||||
test('should not update user without authentication', async () => {
|
||||
const response = await request(server)
|
||||
.patch('/__/users/user')
|
||||
.send({
|
||||
username: 'User2',
|
||||
password: 'password2',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
expect(response.body.success).toBeFalsy();
|
||||
});
|
||||
test('should not delete user without authentication', async () => {
|
||||
const response = await request(server)
|
||||
.delete('/__/users/user')
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(401);
|
||||
expect(response.body.success).toBeFalsy();
|
||||
expect(await User.findOne({ username: 'User1' }).exec()).toBeTruthy();
|
||||
expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeTruthy();
|
||||
expect(await Todo.findOne({ text: 'Todo1' })).toBeTruthy();
|
||||
});
|
||||
test('should delete user', async () => {
|
||||
const response = await request(server)
|
||||
.delete('/__/users/user')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8');
|
||||
expect(response.body.success).toBeTruthy();
|
||||
expect(await User.findOne({ username: 'User1' }).exec()).toBeFalsy();
|
||||
expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeFalsy();
|
||||
expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
require('../../models/Todo');
|
||||
require('../../models/TodoList');
|
||||
require('../../models/User');
|
||||
|
||||
const User = mongoose.model('User');
|
||||
const Todo = mongoose.model('Todo');
|
||||
const TodoList = mongoose.model('TodoList');
|
||||
|
||||
async function seed() {
|
||||
const user = new User({ username: 'User1' });
|
||||
await user.setPassword('password1');
|
||||
await user.save();
|
||||
const token = user.generateJwt();
|
||||
|
||||
const list = new TodoList({ name: 'List1', user: user._id });
|
||||
const todo = new Todo({ text: 'Todo1', list: list._id, user: user._id });
|
||||
|
||||
await list.save();
|
||||
await todo.save();
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
list,
|
||||
todo,
|
||||
};
|
||||
}
|
||||
|
||||
async function clean() {
|
||||
await TodoList.remove({}).exec();
|
||||
await Todo.remove({}).exec();
|
||||
await User.remove({}).exec();
|
||||
}
|
||||
|
||||
const mongodbMemoryServerConfig = {
|
||||
binary: {
|
||||
version: 'latest',
|
||||
},
|
||||
instance: {
|
||||
args: ['--enableMajorityReadConcern=false'],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { seed, clean, mongodbMemoryServerConfig };
|
||||
Reference in New Issue
Block a user