diff --git a/README.md b/README.md index 49c5ece..1d355ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Simple Todo list This is a simple todo list, written in javascript, using express for the backend and react+redux for the frontend. -It also can work in offline thanks to redux-offline (without any conflict resolving, though). +It also can work in offline thanks to redux-offline (without any conflict resolving, though). The code is of somewhat questionable quality, so you probably don't want to look at it. ## Getting started diff --git a/client/package-lock.json b/client/package-lock.json index 30a5601..4742f8c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -4411,11 +4411,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4428,15 +4430,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4539,7 +4544,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4549,6 +4555,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4561,17 +4568,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4588,6 +4598,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4660,7 +4671,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4670,6 +4682,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4775,6 +4788,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/client/src/actions/defs.js b/client/src/actions/defs.js index b251233..1a9da67 100644 --- a/client/src/actions/defs.js +++ b/client/src/actions/defs.js @@ -36,3 +36,7 @@ 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'; diff --git a/client/src/actions/user.js b/client/src/actions/user.js index 6202766..0d44929 100644 --- a/client/src/actions/user.js +++ b/client/src/actions/user.js @@ -7,6 +7,10 @@ import { SIGNUP_FAIL, RESET_USER, LOGOUT, + EDIT_START, + EDIT_SUCCESS, + EDIT_FAIL, + RESET_EDIT, } from './defs'; import { API_ROOT, getToken, setToken } from './util'; @@ -122,6 +126,57 @@ export function signup(user) { }; } + +function startEdit(user) { + return { type: EDIT_START, user }; +} + +function editSuccess(user) { + return { type: EDIT_SUCCESS, user }; +} + +function editFail(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)); + } + }; +} + +export function deleteUser() { + 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 }; +} + export function reset() { return { type: RESET_USER }; } diff --git a/client/src/components/App.js b/client/src/components/App.js index 5729a4b..15d7feb 100644 --- a/client/src/components/App.js +++ b/client/src/components/App.js @@ -10,19 +10,25 @@ import './App.css'; const LoadableTodosView = Loadable({ loader: () => import('./todolist/TodosView'), loading: () => loading, - delay: 200, + delay: 1000, }); const LoadableLoginForm = Loadable({ loader: () => import('./user/LoginForm'), loading: () => loading, - delay: 200, + delay: 1000, }); const LoadableSignupForm = Loadable({ loader: () => import('./user/SignupForm'), loading: () => loading, - delay: 200, + delay: 1000, +}); + +const LoadableEditView = Loadable({ + loader: () => import('./user/EditForm'), + loading: () => loading, + delay: 1000, }); export default class App extends React.PureComponent { @@ -40,6 +46,7 @@ export default class App extends React.PureComponent { + diff --git a/client/src/components/user/EditForm.js b/client/src/components/user/EditForm.js new file mode 100644 index 0000000..d1054a1 --- /dev/null +++ b/client/src/components/user/EditForm.js @@ -0,0 +1,132 @@ +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 './Form.css'; + +import { edit, resetEdit, deleteUser } from '../../actions/user'; + +function validate(values) { + const errors = {}; + if (values.password !== values.passwordRepeat) { + errors.passwordRepeat = 'Passwords should match'; + } + return errors; +} + +function EditForm({ + handleSubmit, + onSubmit, + deleteUser, + user, + history, + reset, +}) { + if (!user.user) { + history.push('/'); + } + console.log(user); + if (user.user && user.editSuccess) { + reset(); + history.push('/'); + } + return ( + +
+ { + history.push('/'); + }} + > + todos + +
+
+
+ + + + +
+ + +
+ +
+
+ ); +} + +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, +}; + +function mapStateToProps(state) { + return { + user: state.user, + }; +} + +function mapDispatchToProps(dispatch) { + 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), + ), +); diff --git a/client/src/components/user/Form.css b/client/src/components/user/Form.css index 6311fa6..a06f927 100644 --- a/client/src/components/user/Form.css +++ b/client/src/components/user/Form.css @@ -31,3 +31,7 @@ form { display: flex; justify-content: space-around; } + +#buttons button { + margin: 0 0.5rem; +} \ No newline at end of file diff --git a/client/src/components/user/HeaderLink.js b/client/src/components/user/HeaderLink.js new file mode 100644 index 0000000..744d532 --- /dev/null +++ b/client/src/components/user/HeaderLink.js @@ -0,0 +1,30 @@ +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 ( + { + e.preventDefault(); + history.push(to); + }} + > + {text} + + ); +} + +Link.propTypes = { + history: PropTypes.any, + to: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, +}; + +export default withRouter(Link); diff --git a/client/src/components/user/UserHeader.js b/client/src/components/user/UserHeader.js index 6ad914d..dbc504f 100644 --- a/client/src/components/user/UserHeader.js +++ b/client/src/components/user/UserHeader.js @@ -3,12 +3,14 @@ import React from 'react'; import LogoutLink from './LogoutLink'; import FetchButton from './FetchButton'; import Status from './Status'; +import HeaderLink from './HeaderLink'; export default function UserHeader() { return (
sync + logout
); diff --git a/client/src/reducers/user.js b/client/src/reducers/user.js index 6bb0926..eb5dfbb 100644 --- a/client/src/reducers/user.js +++ b/client/src/reducers/user.js @@ -7,6 +7,9 @@ import { SIGNUP_SUCCESS, VALIDATE_USER, RESET_USER, + EDIT_SUCCESS, + EDIT_FAIL, + RESET_EDIT, } from '../actions/defs'; export default function user( @@ -40,6 +43,12 @@ export default function user( loaded: true, fetching: false, }; + case EDIT_SUCCESS: + return { + ...state, + user: action.user, + editSuccess: true, + }; case SIGNUP_FAIL: case LOGIN_FAIL: return { @@ -50,6 +59,17 @@ export default function user( 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, diff --git a/package-lock.json b/package-lock.json index 314a1ca..2b33c2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3374,12 +3374,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3394,17 +3396,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3521,7 +3526,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3533,6 +3539,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3547,6 +3554,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3554,12 +3562,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3578,6 +3588,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3658,7 +3669,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3670,6 +3682,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3791,6 +3804,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/routes/users.js b/routes/users.js index f2faf99..8aaea13 100644 --- a/routes/users.js +++ b/routes/users.js @@ -38,16 +38,24 @@ router.patch( '/user', auth.required, asyncHelper(async (req, res) => { - const { username, password } = req.body; + const { username, password, google } = req.body; const patch = {}; - if (username !== undefined) { + if (username !== undefined && username != '') { patch.username = username; } - const user = await User.findOneAndUpdate( - { _id: req.user.id }, - { $set: patch }, - { runValidators: true, context: 'query', new: true }, - ).exec(); + 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}`,