add account edit page

This commit is contained in:
2018-10-07 13:35:30 +03:00
parent 97da946137
commit d19ab57863
12 changed files with 315 additions and 25 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,19 +10,25 @@ import './App.css';
const LoadableTodosView = Loadable({
loader: () => import('./todolist/TodosView'),
loading: () => <span>loading</span>,
delay: 200,
delay: 1000,
});
const LoadableLoginForm = Loadable({
loader: () => import('./user/LoginForm'),
loading: () => <span>loading</span>,
delay: 200,
delay: 1000,
});
const LoadableSignupForm = Loadable({
loader: () => import('./user/SignupForm'),
loading: () => <span>loading</span>,
delay: 200,
delay: 1000,
});
const LoadableEditView = Loadable({
loader: () => import('./user/EditForm'),
loading: () => <span>loading</span>,
delay: 1000,
});
export default class App extends React.PureComponent {
@@ -40,6 +46,7 @@ export default class App extends React.PureComponent {
<Route exact path="/" component={LoadableTodosView} />
<Route path="/login" component={LoadableLoginForm} />
<Route path="/signup" component={LoadableSignupForm} />
<Route path="/edit" component={LoadableEditView} />
</div>
</Router>
</React.Fragment>

View File

@@ -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 (
<React.Fragment>
<div id="user-header">
<ButtonBase
style={{
marginRight: '1rem',
padding: '0 0.5rem',
borderRadius: '7px',
}}
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,
};
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),
),
);

View File

@@ -31,3 +31,7 @@ form {
display: flex;
justify-content: space-around;
}
#buttons button {
margin: 0 0.5rem;
}

View File

@@ -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 (
<ButtonBase
style={{
marginLeft: 'auto',
marginRight: 0,
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,
};
export default withRouter(Link);

View File

@@ -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 (
<div id="user-header">
<FetchButton>sync</FetchButton>
<Status />
<HeaderLink to="/edit" text="edit"/>
<LogoutLink>logout</LogoutLink>
</div>
);

View File

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

28
package-lock.json generated
View File

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

View File

@@ -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}`,