Skip to content

Commit fe6acc9

Browse files
yining1023catarak
authored andcommitted
Adding User Settings View (#325)
* added account page showing username and email * change username and email * validate current password and add new password * reject promise with error for reduxForm submit-validation for current password * updated user reducer to handle setting sucess and server side async * warning if there is current password but no new password * fixes logout button * import validate function, fixes logout style
1 parent 6af92a4 commit fe6acc9

File tree

13 files changed

+317
-46
lines changed

13 files changed

+317
-46
lines changed

client/components/Nav.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ class Nav extends React.PureComponent {
136136
My sketches
137137
</Link>
138138
</li>
139+
<li>
140+
<Link to={`/${this.props.user.username}/account`}>
141+
My account
142+
</Link>
143+
</li>
139144
<li>
140145
<button onClick={this.props.logoutUser} >
141146
Log out

client/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const AUTH_USER = 'AUTH_USER';
2323
export const UNAUTH_USER = 'UNAUTH_USER';
2424
export const AUTH_ERROR = 'AUTH_ERROR';
2525

26+
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
27+
2628
export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';
2729

2830
export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS';
@@ -96,6 +98,7 @@ export const RESET_INFINITE_LOOPS = 'RESET_INFINITE_LOOPS';
9698
export const RESET_PASSWORD_INITIATE = 'RESET_PASSWORD_INITIATE';
9799
export const RESET_PASSWORD_RESET = 'RESET_PASSWORD_RESET';
98100
export const INVALID_RESET_PASSWORD_TOKEN = 'INVALID_RESET_PASSWORD_TOKEN';
101+
99102
// eventually, handle errors more specifically and better
100103
export const ERROR = 'ERROR';
101104

client/modules/User/actions.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,20 @@ export function updatePassword(token, formValues) {
160160
}));
161161
};
162162
}
163+
164+
export function updateSettingsSuccess(user) {
165+
return {
166+
type: ActionTypes.SETTINGS_UPDATED,
167+
user
168+
};
169+
}
170+
171+
export function updateSettings(formValues) {
172+
return dispatch =>
173+
axios.put(`${ROOT_URL}/account`, formValues, { withCredentials: true })
174+
.then((response) => {
175+
dispatch(updateSettingsSuccess(response.data));
176+
browserHistory.push('/');
177+
})
178+
.catch(response => Promise.reject({ currentPassword: response.data.error }));
179+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { PropTypes } from 'react';
2+
import { domOnlyProps } from '../../../utils/reduxFormUtils';
3+
4+
function AccountForm(props) {
5+
const {
6+
fields: { username, email, currentPassword, newPassword },
7+
handleSubmit,
8+
submitting,
9+
invalid,
10+
pristine
11+
} = props;
12+
return (
13+
<form className="form" onSubmit={handleSubmit(props.updateSettings)}>
14+
<p className="form__field">
15+
<label htmlFor="email" className="form__label">Email</label>
16+
<input
17+
className="form__input"
18+
aria-label="email"
19+
type="text"
20+
id="email"
21+
{...domOnlyProps(email)}
22+
/>
23+
{email.touched && email.error && <span className="form-error">{email.error}</span>}
24+
</p>
25+
<p className="form__field">
26+
<label htmlFor="username" className="form__label">User Name</label>
27+
<input
28+
className="form__input"
29+
aria-label="username"
30+
type="text"
31+
id="username"
32+
defaultValue={username}
33+
{...domOnlyProps(username)}
34+
/>
35+
{username.touched && username.error && <span className="form-error">{username.error}</span>}
36+
</p>
37+
<p className="form__field">
38+
<label htmlFor="current password" className="form__label">Current Password</label>
39+
<input
40+
className="form__input"
41+
aria-label="currentPassword"
42+
type="password"
43+
id="currentPassword"
44+
{...domOnlyProps(currentPassword)}
45+
/>
46+
{currentPassword.touched && currentPassword.error && <span className="form-error">{currentPassword.error}</span>}
47+
</p>
48+
<p className="form__field">
49+
<label htmlFor="new password" className="form__label">New Password</label>
50+
<input
51+
className="form__input"
52+
aria-label="newPassword"
53+
type="password"
54+
id="newPassword"
55+
{...domOnlyProps(newPassword)}
56+
/>
57+
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>}
58+
</p>
59+
<input type="submit" disabled={submitting || invalid || pristine} value="Save All Settings" aria-label="updateSettings" />
60+
</form>
61+
);
62+
}
63+
64+
AccountForm.propTypes = {
65+
fields: PropTypes.shape({
66+
username: PropTypes.object.isRequired,
67+
email: PropTypes.object.isRequired,
68+
currentPassword: PropTypes.object.isRequired,
69+
newPassword: PropTypes.object.isRequired
70+
}).isRequired,
71+
handleSubmit: PropTypes.func.isRequired,
72+
updateSettings: PropTypes.func.isRequired,
73+
submitting: PropTypes.bool,
74+
invalid: PropTypes.bool,
75+
pristine: PropTypes.bool,
76+
};
77+
78+
AccountForm.defaultProps = {
79+
submitting: false,
80+
pristine: true,
81+
invalid: false
82+
};
83+
84+
export default AccountForm;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { PropTypes } from 'react';
2+
import { reduxForm } from 'redux-form';
3+
import { bindActionCreators } from 'redux';
4+
import { browserHistory } from 'react-router';
5+
import InlineSVG from 'react-inlinesvg';
6+
import axios from 'axios';
7+
import { updateSettings } from '../actions';
8+
import AccountForm from '../components/AccountForm';
9+
import { validateSettings } from '../../../utils/reduxFormUtils';
10+
11+
const exitUrl = require('../../../images/exit.svg');
12+
const logoUrl = require('../../../images/p5js-logo.svg');
13+
14+
15+
class AccountView extends React.Component {
16+
constructor(props) {
17+
super(props);
18+
this.closeAccountPage = this.closeAccountPage.bind(this);
19+
this.gotoHomePage = this.gotoHomePage.bind(this);
20+
}
21+
22+
closeAccountPage() {
23+
browserHistory.push(this.props.previousPath);
24+
}
25+
26+
gotoHomePage() {
27+
browserHistory.push('/');
28+
}
29+
30+
render() {
31+
return (
32+
<div className="form-container">
33+
<div className="form-container__header">
34+
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
35+
<InlineSVG src={logoUrl} alt="p5js Logo" />
36+
</button>
37+
<button className="form-container__exit-button" onClick={this.closeAccountPage}>
38+
<InlineSVG src={exitUrl} alt="Close Account Page" />
39+
</button>
40+
</div>
41+
<div className="form-container__content">
42+
<h2 className="form-container__title">My Account</h2>
43+
<AccountForm {...this.props} />
44+
{/* <h2 className="form-container__divider">Or</h2>
45+
<GithubButton buttonText="Login with Github" /> */}
46+
</div>
47+
</div>
48+
);
49+
}
50+
}
51+
52+
function mapStateToProps(state) {
53+
return {
54+
initialValues: state.user, // <- initialValues for reduxForm
55+
user: state.user,
56+
previousPath: state.ide.previousPath
57+
};
58+
}
59+
60+
function mapDispatchToProps(dispatch) {
61+
return bindActionCreators({ updateSettings }, dispatch);
62+
}
63+
64+
function asyncValidate(formProps, dispatch, props) {
65+
const fieldToValidate = props.form._active;
66+
if (fieldToValidate) {
67+
const queryParams = {};
68+
queryParams[fieldToValidate] = formProps[fieldToValidate];
69+
queryParams.check_type = fieldToValidate;
70+
return axios.get('/api/signup/duplicate_check', { params: queryParams })
71+
.then((response) => {
72+
if (response.data.exists) {
73+
const error = {};
74+
error[fieldToValidate] = response.data.message;
75+
throw error;
76+
}
77+
});
78+
}
79+
return Promise.resolve(true).then(() => {});
80+
}
81+
82+
AccountView.propTypes = {
83+
previousPath: PropTypes.string.isRequired
84+
};
85+
86+
export default reduxForm({
87+
form: 'updateAllSettings',
88+
fields: ['username', 'email', 'currentPassword', 'newPassword'],
89+
validate: validateSettings,
90+
asyncValidate,
91+
asyncBlurFields: ['username', 'email', 'currentPassword']
92+
}, mapStateToProps, mapDispatchToProps)(AccountView);

client/modules/User/pages/LoginView.jsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Link, browserHistory } from 'react-router';
44
import InlineSVG from 'react-inlinesvg';
55
import { validateAndLoginUser } from '../actions';
66
import LoginForm from '../components/LoginForm';
7+
import { validateLogin } from '../../../utils/reduxFormUtils';
78
// import GithubButton from '../components/GithubButton';
89
const exitUrl = require('../../../images/exit.svg');
910
const logoUrl = require('../../../images/p5js-logo.svg');
@@ -67,23 +68,12 @@ function mapDispatchToProps() {
6768
};
6869
}
6970

70-
function validate(formProps) {
71-
const errors = {};
72-
if (!formProps.email) {
73-
errors.email = 'Please enter an email';
74-
}
75-
if (!formProps.password) {
76-
errors.password = 'Please enter a password';
77-
}
78-
return errors;
79-
}
80-
8171
LoginView.propTypes = {
8272
previousPath: PropTypes.string.isRequired
8373
};
8474

8575
export default reduxForm({
8676
form: 'login',
8777
fields: ['email', 'password'],
88-
validate
78+
validate: validateLogin
8979
}, mapStateToProps, mapDispatchToProps)(LoginView);

client/modules/User/pages/SignupView.jsx

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import InlineSVG from 'react-inlinesvg';
66
import { reduxForm } from 'redux-form';
77
import * as UserActions from '../actions';
88
import SignupForm from '../components/SignupForm';
9+
import { validateSignup } from '../../../utils/reduxFormUtils';
910

1011
const exitUrl = require('../../../images/exit.svg');
1112
const logoUrl = require('../../../images/p5js-logo.svg');
@@ -78,37 +79,6 @@ function asyncValidate(formProps, dispatch, props) {
7879
return Promise.resolve(true).then(() => {});
7980
}
8081

81-
function validate(formProps) {
82-
const errors = {};
83-
84-
if (!formProps.username) {
85-
errors.username = 'Please enter a username.';
86-
} else if (!formProps.username.match(/^.{1,20}$/)) {
87-
errors.username = 'Username must be less than 20 characters.';
88-
} else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) {
89-
errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.';
90-
}
91-
92-
if (!formProps.email) {
93-
errors.email = 'Please enter an email.';
94-
} else if (!formProps.email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) {
95-
errors.email = 'Please enter a valid email address.';
96-
}
97-
98-
if (!formProps.password) {
99-
errors.password = 'Please enter a password';
100-
}
101-
if (!formProps.confirmPassword) {
102-
errors.confirmPassword = 'Please enter a password confirmation';
103-
}
104-
105-
if (formProps.password !== formProps.confirmPassword) {
106-
errors.password = 'Passwords must match';
107-
}
108-
109-
return errors;
110-
}
111-
11282
function onSubmitFail(errors) {
11383
console.log(errors);
11484
}
@@ -121,7 +91,7 @@ export default reduxForm({
12191
form: 'signup',
12292
fields: ['username', 'email', 'password', 'confirmPassword'],
12393
onSubmitFail,
124-
validate,
94+
validate: validateSignup,
12595
asyncValidate,
12696
asyncBlurFields: ['username', 'email']
12797
}, mapStateToProps, mapDispatchToProps)(SignupView);

client/modules/User/reducers.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const user = (state = { authenticated: false }, action) => {
1919
return Object.assign({}, state, { resetPasswordInitiate: false });
2020
case ActionTypes.INVALID_RESET_PASSWORD_TOKEN:
2121
return Object.assign({}, state, { resetPasswordInvalid: true });
22+
case ActionTypes.SETTINGS_UPDATED:
23+
return { ...state, ...action.user };
2224
default:
2325
return state;
2426
}

client/routes.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import LoginView from './modules/User/pages/LoginView';
77
import SignupView from './modules/User/pages/SignupView';
88
import ResetPasswordView from './modules/User/pages/ResetPasswordView';
99
import NewPasswordView from './modules/User/pages/NewPasswordView';
10+
import AccountView from './modules/User/pages/AccountView';
1011
// import SketchListView from './modules/Sketch/pages/SketchListView';
1112
import { getUser } from './modules/User/actions';
1213

@@ -27,6 +28,7 @@ const routes = store =>
2728
<Route path="/sketches" component={IDEView} />
2829
<Route path="/:username/sketches/:project_id" component={IDEView} />
2930
<Route path="/:username/sketches" component={IDEView} />
31+
<Route path="/:username/account" component={AccountView} />
3032
<Route path="/about" component={IDEView} />
3133
</Route>
3234
);

client/styles/components/_nav.scss

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
.nav__item-spacer {
5757
@include themify() {
5858
color: map-get($theme-map, 'inactive-text-color');
59-
}
59+
}
6060
padding: 0 #{15 / $base-font-size}rem;
6161
}
6262

@@ -65,12 +65,16 @@
6565
width: 100%;
6666
}
6767

68-
.nav__dropdown a {
68+
.nav__dropdown a, button {
6969
@include themify() {
7070
color: getThemifyVariable('secondary-text-color');
7171
}
7272
}
7373

74+
.nav__dropdown button {
75+
padding: 0;
76+
}
77+
7478
.nav__dropdown a:hover {
7579
@include themify() {
7680
color: getThemifyVariable('primary-text-color');

0 commit comments

Comments
 (0)