mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 07:37:49 +01:00
add account edit page
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
28
client/package-lock.json
generated
28
client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
132
client/src/components/user/EditForm.js
Normal file
132
client/src/components/user/EditForm.js
Normal 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),
|
||||
),
|
||||
);
|
||||
@@ -31,3 +31,7 @@ form {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
30
client/src/components/user/HeaderLink.js
Normal file
30
client/src/components/user/HeaderLink.js
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
28
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user