Skip to content

Adding User Settings View #325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions client/components/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ class Nav extends React.PureComponent {
My sketches
</Link>
</li>
<li>
<Link to={`/${this.props.user.username}/account`}>
My account
</Link>
</li>
<li>
<button onClick={this.props.logoutUser} >
Log out
Expand Down
3 changes: 3 additions & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const AUTH_USER = 'AUTH_USER';
export const UNAUTH_USER = 'UNAUTH_USER';
export const AUTH_ERROR = 'AUTH_ERROR';

export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';

export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';

export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS';
Expand Down Expand Up @@ -96,6 +98,7 @@ export const RESET_INFINITE_LOOPS = 'RESET_INFINITE_LOOPS';
export const RESET_PASSWORD_INITIATE = 'RESET_PASSWORD_INITIATE';
export const RESET_PASSWORD_RESET = 'RESET_PASSWORD_RESET';
export const INVALID_RESET_PASSWORD_TOKEN = 'INVALID_RESET_PASSWORD_TOKEN';

// eventually, handle errors more specifically and better
export const ERROR = 'ERROR';

Expand Down
17 changes: 17 additions & 0 deletions client/modules/User/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,20 @@ export function updatePassword(token, formValues) {
}));
};
}

export function updateSettingsSuccess(user) {
return {
type: ActionTypes.SETTINGS_UPDATED,
user
};
}

export function updateSettings(formValues) {
return dispatch =>
axios.put(`${ROOT_URL}/account`, formValues, { withCredentials: true })
.then((response) => {
dispatch(updateSettingsSuccess(response.data));
browserHistory.push('/');
})
.catch(response => Promise.reject({ currentPassword: response.data.error }));
}
84 changes: 84 additions & 0 deletions client/modules/User/components/AccountForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { PropTypes } from 'react';
import { domOnlyProps } from '../../../utils/reduxFormUtils';

function AccountForm(props) {
const {
fields: { username, email, currentPassword, newPassword },
handleSubmit,
submitting,
invalid,
pristine
} = props;
return (
<form className="form" onSubmit={handleSubmit(props.updateSettings)}>
<p className="form__field">
<label htmlFor="email" className="form__label">Email</label>
<input
className="form__input"
aria-label="email"
type="text"
id="email"
{...domOnlyProps(email)}
/>
{email.touched && email.error && <span className="form-error">{email.error}</span>}
</p>
<p className="form__field">
<label htmlFor="username" className="form__label">User Name</label>
<input
className="form__input"
aria-label="username"
type="text"
id="username"
defaultValue={username}
{...domOnlyProps(username)}
/>
{username.touched && username.error && <span className="form-error">{username.error}</span>}
</p>
<p className="form__field">
<label htmlFor="current password" className="form__label">Current Password</label>
<input
className="form__input"
aria-label="currentPassword"
type="password"
id="currentPassword"
{...domOnlyProps(currentPassword)}
/>
{currentPassword.touched && currentPassword.error && <span className="form-error">{currentPassword.error}</span>}
</p>
<p className="form__field">
<label htmlFor="new password" className="form__label">New Password</label>
<input
className="form__input"
aria-label="newPassword"
type="password"
id="newPassword"
{...domOnlyProps(newPassword)}
/>
{newPassword.touched && newPassword.error && <span className="form-error">{newPassword.error}</span>}
</p>
<input type="submit" disabled={submitting || invalid || pristine} value="Save All Settings" aria-label="updateSettings" />
</form>
);
}

AccountForm.propTypes = {
fields: PropTypes.shape({
username: PropTypes.object.isRequired,
email: PropTypes.object.isRequired,
currentPassword: PropTypes.object.isRequired,
newPassword: PropTypes.object.isRequired
}).isRequired,
handleSubmit: PropTypes.func.isRequired,
updateSettings: PropTypes.func.isRequired,
submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool,
};

AccountForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
};

export default AccountForm;
92 changes: 92 additions & 0 deletions client/modules/User/pages/AccountView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import { bindActionCreators } from 'redux';
import { browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import axios from 'axios';
import { updateSettings } from '../actions';
import AccountForm from '../components/AccountForm';
import { validateSettings } from '../../../utils/reduxFormUtils';

const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');


class AccountView extends React.Component {
constructor(props) {
super(props);
this.closeAccountPage = this.closeAccountPage.bind(this);
this.gotoHomePage = this.gotoHomePage.bind(this);
}

closeAccountPage() {
browserHistory.push(this.props.previousPath);
}

gotoHomePage() {
browserHistory.push('/');
}

render() {
return (
<div className="form-container">
<div className="form-container__header">
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
<InlineSVG src={logoUrl} alt="p5js Logo" />
</button>
<button className="form-container__exit-button" onClick={this.closeAccountPage}>
<InlineSVG src={exitUrl} alt="Close Account Page" />
</button>
</div>
<div className="form-container__content">
<h2 className="form-container__title">My Account</h2>
<AccountForm {...this.props} />
{/* <h2 className="form-container__divider">Or</h2>
<GithubButton buttonText="Login with Github" /> */}
</div>
</div>
);
}
}

function mapStateToProps(state) {
return {
initialValues: state.user, // <- initialValues for reduxForm
user: state.user,
previousPath: state.ide.previousPath
};
}

function mapDispatchToProps(dispatch) {
return bindActionCreators({ updateSettings }, dispatch);
}

function asyncValidate(formProps, dispatch, props) {
const fieldToValidate = props.form._active;
if (fieldToValidate) {
const queryParams = {};
queryParams[fieldToValidate] = formProps[fieldToValidate];
queryParams.check_type = fieldToValidate;
return axios.get('/api/signup/duplicate_check', { params: queryParams })
.then((response) => {
if (response.data.exists) {
const error = {};
error[fieldToValidate] = response.data.message;
throw error;
}
});
}
return Promise.resolve(true).then(() => {});
}

AccountView.propTypes = {
previousPath: PropTypes.string.isRequired
};

export default reduxForm({
form: 'updateAllSettings',
fields: ['username', 'email', 'currentPassword', 'newPassword'],
validate: validateSettings,
asyncValidate,
asyncBlurFields: ['username', 'email', 'currentPassword']
}, mapStateToProps, mapDispatchToProps)(AccountView);
14 changes: 2 additions & 12 deletions client/modules/User/pages/LoginView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import { validateAndLoginUser } from '../actions';
import LoginForm from '../components/LoginForm';
import { validateLogin } from '../../../utils/reduxFormUtils';
// import GithubButton from '../components/GithubButton';
const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
Expand Down Expand Up @@ -67,23 +68,12 @@ function mapDispatchToProps() {
};
}

function validate(formProps) {
const errors = {};
if (!formProps.email) {
errors.email = 'Please enter an email';
}
if (!formProps.password) {
errors.password = 'Please enter a password';
}
return errors;
}

LoginView.propTypes = {
previousPath: PropTypes.string.isRequired
};

export default reduxForm({
form: 'login',
fields: ['email', 'password'],
validate
validate: validateLogin
}, mapStateToProps, mapDispatchToProps)(LoginView);
34 changes: 2 additions & 32 deletions client/modules/User/pages/SignupView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import InlineSVG from 'react-inlinesvg';
import { reduxForm } from 'redux-form';
import * as UserActions from '../actions';
import SignupForm from '../components/SignupForm';
import { validateSignup } from '../../../utils/reduxFormUtils';

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

function validate(formProps) {
const errors = {};

if (!formProps.username) {
errors.username = 'Please enter a username.';
} else if (!formProps.username.match(/^.{1,20}$/)) {
errors.username = 'Username must be less than 20 characters.';
} else if (!formProps.username.match(/^[a-zA-Z0-9._-]{1,20}$/)) {
errors.username = 'Username must only consist of numbers, letters, periods, dashes, and underscores.';
}

if (!formProps.email) {
errors.email = 'Please enter an email.';
} 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)) {
errors.email = 'Please enter a valid email address.';
}

if (!formProps.password) {
errors.password = 'Please enter a password';
}
if (!formProps.confirmPassword) {
errors.confirmPassword = 'Please enter a password confirmation';
}

if (formProps.password !== formProps.confirmPassword) {
errors.password = 'Passwords must match';
}

return errors;
}

function onSubmitFail(errors) {
console.log(errors);
}
Expand All @@ -121,7 +91,7 @@ export default reduxForm({
form: 'signup',
fields: ['username', 'email', 'password', 'confirmPassword'],
onSubmitFail,
validate,
validate: validateSignup,
asyncValidate,
asyncBlurFields: ['username', 'email']
}, mapStateToProps, mapDispatchToProps)(SignupView);
2 changes: 2 additions & 0 deletions client/modules/User/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const user = (state = { authenticated: false }, action) => {
return Object.assign({}, state, { resetPasswordInitiate: false });
case ActionTypes.INVALID_RESET_PASSWORD_TOKEN:
return Object.assign({}, state, { resetPasswordInvalid: true });
case ActionTypes.SETTINGS_UPDATED:
return { ...state, ...action.user };
default:
return state;
}
Expand Down
2 changes: 2 additions & 0 deletions client/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import LoginView from './modules/User/pages/LoginView';
import SignupView from './modules/User/pages/SignupView';
import ResetPasswordView from './modules/User/pages/ResetPasswordView';
import NewPasswordView from './modules/User/pages/NewPasswordView';
import AccountView from './modules/User/pages/AccountView';
// import SketchListView from './modules/Sketch/pages/SketchListView';
import { getUser } from './modules/User/actions';

Expand All @@ -27,6 +28,7 @@ const routes = store =>
<Route path="/sketches" component={IDEView} />
<Route path="/:username/sketches/:project_id" component={IDEView} />
<Route path="/:username/sketches" component={IDEView} />
<Route path="/:username/account" component={AccountView} />
<Route path="/about" component={IDEView} />
</Route>
);
Expand Down
8 changes: 6 additions & 2 deletions client/styles/components/_nav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
.nav__item-spacer {
@include themify() {
color: map-get($theme-map, 'inactive-text-color');
}
}
padding: 0 #{15 / $base-font-size}rem;
}

Expand All @@ -65,12 +65,16 @@
width: 100%;
}

.nav__dropdown a {
.nav__dropdown a, button {
@include themify() {
color: getThemifyVariable('secondary-text-color');
}
}

.nav__dropdown button {
padding: 0;
}

.nav__dropdown a:hover {
@include themify() {
color: getThemifyVariable('primary-text-color');
Expand Down
Loading