Feature(settings): complete settings page logic
This commit is contained in:
@ -9,7 +9,7 @@ export const updateTitle = createAction(types.updateTitle);
|
|||||||
// used in combination with fetch-user-saga
|
// used in combination with fetch-user-saga
|
||||||
export const fetchUser = createAction(types.fetchUser);
|
export const fetchUser = createAction(types.fetchUser);
|
||||||
|
|
||||||
// setUser(
|
// addUser(
|
||||||
// entities: { [userId]: User }
|
// entities: { [userId]: User }
|
||||||
// ) => Action
|
// ) => Action
|
||||||
export const addUser = createAction(
|
export const addUser = createAction(
|
||||||
@ -25,11 +25,21 @@ export const updateUserPoints = createAction(
|
|||||||
types.updateUserPoints,
|
types.updateUserPoints,
|
||||||
(username, points) => ({ username, points })
|
(username, points) => ({ username, points })
|
||||||
);
|
);
|
||||||
// updateUserPoints(username: String, flag: String) => Action
|
// updateUserFlag(username: String, flag: String) => Action
|
||||||
export const updateUserFlag = createAction(
|
export const updateUserFlag = createAction(
|
||||||
types.updateUserFlag,
|
types.updateUserFlag,
|
||||||
(username, flag) => ({ username, flag })
|
(username, flag) => ({ username, flag })
|
||||||
);
|
);
|
||||||
|
// updateUserEmail(username: String, email: String) => Action
|
||||||
|
export const updateUserEmail = createAction(
|
||||||
|
types.updateUserFlag,
|
||||||
|
(username, email) => ({ username, email })
|
||||||
|
);
|
||||||
|
// updateUserLang(username: String, lang: String) => Action
|
||||||
|
export const updateUserLang = createAction(
|
||||||
|
types.updateUserLang,
|
||||||
|
(username, lang) => ({ username, lang })
|
||||||
|
);
|
||||||
// updateCompletedChallenges(username: String) => Action
|
// updateCompletedChallenges(username: String) => Action
|
||||||
export const updateCompletedChallenges = createAction(
|
export const updateCompletedChallenges = createAction(
|
||||||
types.updateCompletedChallenges
|
types.updateCompletedChallenges
|
||||||
@ -55,6 +65,16 @@ export const createErrorObservable = error => Observable.just({
|
|||||||
type: types.handleError,
|
type: types.handleError,
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
|
// doActionOnError(
|
||||||
|
// actionCreator: (() => Action|Null)
|
||||||
|
// ) => (error: Error) => Observable[Action]
|
||||||
|
export const doActionOnError = actionCreator => error => Observable.of(
|
||||||
|
{
|
||||||
|
type: types.handleError,
|
||||||
|
error
|
||||||
|
},
|
||||||
|
actionCreator()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// drawers
|
// drawers
|
||||||
|
@ -8,8 +8,13 @@ const initialState = {
|
|||||||
user: {}
|
user: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// future refactor(berks): Several of the actions here are just updating props
|
||||||
|
// on the main user. These can be refactors into one response for all actions
|
||||||
export default function entities(state = initialState, action) {
|
export default function entities(state = initialState, action) {
|
||||||
const { type, payload: { username, points, flag } = {} } = action;
|
const {
|
||||||
|
type,
|
||||||
|
payload: { email, username, points, flag, languageTag } = {}
|
||||||
|
} = action;
|
||||||
if (type === updateCompletedChallenges) {
|
if (type === updateCompletedChallenges) {
|
||||||
const username = action.payload;
|
const username = action.payload;
|
||||||
const completedChallengeMap = state.user[username].challengeMap || {};
|
const completedChallengeMap = state.user[username].challengeMap || {};
|
||||||
@ -26,6 +31,12 @@ export default function entities(state = initialState, action) {
|
|||||||
}, {})
|
}, {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (action.meta && action.meta.entities) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...action.meta.entities
|
||||||
|
};
|
||||||
|
}
|
||||||
if (type === updateUserPoints) {
|
if (type === updateUserPoints) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -38,12 +49,6 @@ export default function entities(state = initialState, action) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.meta && action.meta.entities) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.meta.entities
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === types.updateUserFlag) {
|
if (action.type === types.updateUserFlag) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -56,5 +61,29 @@ export default function entities(state = initialState, action) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (action.type === types.updateUserEmail) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (action.type === types.updateUserLang) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
languageTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ export default createTypes([
|
|||||||
'updateThisUser',
|
'updateThisUser',
|
||||||
'updateUserPoints',
|
'updateUserPoints',
|
||||||
'updateUserFlag',
|
'updateUserFlag',
|
||||||
|
'updateUserEmail',
|
||||||
|
'updateUserLang',
|
||||||
'updateCompletedChallenges',
|
'updateCompletedChallenges',
|
||||||
'showSignIn',
|
'showSignIn',
|
||||||
|
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
import { Button, Row, Col } from 'react-bootstrap';
|
import { Button, Row, Col } from 'react-bootstrap';
|
||||||
import FA from 'react-fontawesome';
|
import FA from 'react-fontawesome';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
export function UpdateEmailButton() {
|
export function UpdateEmailButton() {
|
||||||
return (
|
return (
|
||||||
|
<Link to='/settings/update-email'>
|
||||||
<Button
|
<Button
|
||||||
block={ true }
|
block={ true }
|
||||||
bsSize='lg'
|
bsSize='lg'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
className='btn-link-social'
|
className='btn-link-social'
|
||||||
href='/update-email'
|
|
||||||
>
|
>
|
||||||
<FA name='envelope' />
|
<FA name='envelope' />
|
||||||
Update my Email
|
Update my Email
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,27 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
import { FormControl } from 'react-bootstrap';
|
import { FormControl } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import { updateMyLang } from '../redux/actions';
|
||||||
|
import { userSelector } from '../../../redux/selectors';
|
||||||
import langs from '../../../../utils/supported-languages';
|
import langs from '../../../../utils/supported-languages';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({ user: { languageTag } }) => ({
|
||||||
|
// send null to prevent redux-form from initialize empty
|
||||||
|
initialValues: languageTag ? { lang: languageTag } : null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const actions = { updateMyLang };
|
||||||
|
const fields = [ 'lang' ];
|
||||||
|
const validator = values => {
|
||||||
|
if (!langs[values.lang]) {
|
||||||
|
return { lang: `${values.lang} is unsupported` };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
const options = [(
|
const options = [(
|
||||||
<option
|
<option
|
||||||
disabled={ true }
|
disabled={ true }
|
||||||
@ -30,19 +50,58 @@ const options = [(
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function LangaugeSettings({ userLang }) {
|
export class LangaugeSettings extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
fields: PropTypes.object,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
updateMyLang: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
constructor(...props) {
|
||||||
|
super(...props);
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
// make sure to clear timeout if it exist
|
||||||
|
if (this.timeout) {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.fields.lang.onChange(e);
|
||||||
|
// give redux-form HOC state time to catch up before
|
||||||
|
// attempting to submit
|
||||||
|
this.timeout = setTimeout(
|
||||||
|
() => this.props.handleSubmit(this.props.updateMyLang)(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fields: { lang }
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<FormControl
|
<FormControl
|
||||||
className='btn btn-block btn-primary btn-link-social'
|
className='btn btn-block btn-primary btn-link-social'
|
||||||
componentClass='select'
|
componentClass='select'
|
||||||
defaultValue='not-the-momma'
|
{ ...lang }
|
||||||
value={ userLang }
|
onChange={ this.handleChange }
|
||||||
>
|
>
|
||||||
{ options }
|
{ options }
|
||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LangaugeSettings.propTypes = {
|
export default reduxForm(
|
||||||
userLang: PropTypes.string
|
{
|
||||||
};
|
form: 'lang',
|
||||||
|
fields,
|
||||||
|
validator,
|
||||||
|
overwriteOnInitialValuesChange: false
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
actions
|
||||||
|
)(LangaugeSettings);
|
||||||
|
@ -14,9 +14,13 @@ import {
|
|||||||
openDeleteModal,
|
openDeleteModal,
|
||||||
hideDeleteModal
|
hideDeleteModal
|
||||||
} from '../redux/actions';
|
} from '../redux/actions';
|
||||||
import { toggleNightMode } from '../../../redux/actions';
|
import {
|
||||||
|
toggleNightMode,
|
||||||
|
updateTitle
|
||||||
|
} from '../../../redux/actions';
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
updateTitle,
|
||||||
toggleNightMode,
|
toggleNightMode,
|
||||||
openDeleteModal,
|
openDeleteModal,
|
||||||
hideDeleteModal,
|
hideDeleteModal,
|
||||||
@ -57,8 +61,13 @@ const mapStateToProps = state => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class Settings extends React.Component {
|
export class Settings extends React.Component {
|
||||||
|
constructor(...props) {
|
||||||
|
super(...props);
|
||||||
|
this.updateMyLang = this.updateMyLang.bind(this);
|
||||||
|
}
|
||||||
static displayName = 'Settings';
|
static displayName = 'Settings';
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
children: PropTypes.element,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
isDeleteOpen: PropTypes.bool,
|
isDeleteOpen: PropTypes.bool,
|
||||||
isLocked: PropTypes.bool,
|
isLocked: PropTypes.bool,
|
||||||
@ -69,17 +78,32 @@ export class Settings extends React.Component {
|
|||||||
sendMonthlyEmail: PropTypes.bool,
|
sendMonthlyEmail: PropTypes.bool,
|
||||||
sendNotificationEmail: PropTypes.bool,
|
sendNotificationEmail: PropTypes.bool,
|
||||||
sendQuincyEmail: PropTypes.bool,
|
sendQuincyEmail: PropTypes.bool,
|
||||||
toggleNightMode: PropTypes.func,
|
updateTitle: PropTypes.func.isRequired,
|
||||||
toggleIsLocked: PropTypes.func,
|
toggleNightMode: PropTypes.func.isRequired,
|
||||||
toggleQuincyEmail: PropTypes.func,
|
toggleIsLocked: PropTypes.func.isRequired,
|
||||||
toggleMonthlyEmail: PropTypes.func,
|
toggleQuincyEmail: PropTypes.func.isRequired,
|
||||||
toggleNotificationEmail: PropTypes.func,
|
toggleMonthlyEmail: PropTypes.func.isRequired,
|
||||||
openDeleteModal: PropTypes.func,
|
toggleNotificationEmail: PropTypes.func.isRequired,
|
||||||
hideDeleteModal: PropTypes.func
|
openDeleteModal: PropTypes.func.isRequired,
|
||||||
|
hideDeleteModal: PropTypes.func.isRequired,
|
||||||
|
lang: PropTypes.string,
|
||||||
|
initialLang: PropTypes.string,
|
||||||
|
updateMyLang: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateMyLang(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const lang = e.target.value;
|
||||||
|
this.props.updateMyLang(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.props.updateTitle('Settings');
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
children,
|
||||||
username,
|
username,
|
||||||
isDeleteOpen,
|
isDeleteOpen,
|
||||||
isLocked,
|
isLocked,
|
||||||
@ -98,6 +122,18 @@ export class Settings extends React.Component {
|
|||||||
openDeleteModal,
|
openDeleteModal,
|
||||||
hideDeleteModal
|
hideDeleteModal
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
if (children) {
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
sm={ 4 }
|
||||||
|
smOffset={ 4 }
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import Settings from './components/Settings.jsx';
|
import Settings from './components/Settings.jsx';
|
||||||
|
import updateEmail from './routes/update-email';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
component: Settings
|
component: Settings,
|
||||||
|
childRoutes: [
|
||||||
|
updateEmail
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
@ -8,12 +8,19 @@ const initialState = {
|
|||||||
export const types = createTypes([
|
export const types = createTypes([
|
||||||
'toggleUserFlag',
|
'toggleUserFlag',
|
||||||
'openDeleteModal',
|
'openDeleteModal',
|
||||||
'hideDeleteModal'
|
'hideDeleteModal',
|
||||||
|
'updateMyEmail',
|
||||||
|
'updateMyLang'
|
||||||
], 'settings');
|
], 'settings');
|
||||||
|
|
||||||
export const toggleUserFlag = createAction(types.toggleUserFlag);
|
export const toggleUserFlag = createAction(types.toggleUserFlag);
|
||||||
export const openDeleteModal = createAction(types.openDeleteModal);
|
export const openDeleteModal = createAction(types.openDeleteModal);
|
||||||
export const hideDeleteModal = createAction(types.hideDeleteModal);
|
export const hideDeleteModal = createAction(types.hideDeleteModal);
|
||||||
|
export const updateMyEmail = createAction(types.updateMyEmail);
|
||||||
|
export const updateMyLang = createAction(
|
||||||
|
types.updateMyLang,
|
||||||
|
(values) => values.lang
|
||||||
|
);
|
||||||
|
|
||||||
export default handleActions(
|
export default handleActions(
|
||||||
{
|
{
|
||||||
|
0
common/app/routes/settings/redux/selectors.js
Normal file
0
common/app/routes/settings/redux/selectors.js
Normal file
@ -1,7 +1,16 @@
|
|||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
import { types } from './actions';
|
import { types } from './actions';
|
||||||
|
import combineSagas from '../../../utils/combine-sagas';
|
||||||
|
import { makeToast } from '../../../toasts/redux/actions';
|
||||||
|
import {
|
||||||
|
updateUserFlag,
|
||||||
|
updateUserEmail,
|
||||||
|
updateUserLang,
|
||||||
|
doActionOnError
|
||||||
|
} from '../../../redux/actions';
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
import { updateUserFlag, createErrorObservable } from '../../../redux/actions';
|
import langs from '../../../../utils/supported-languages';
|
||||||
|
|
||||||
const urlMap = {
|
const urlMap = {
|
||||||
isLocked: 'lockdown',
|
isLocked: 'lockdown',
|
||||||
@ -9,7 +18,61 @@ const urlMap = {
|
|||||||
sendNotificationEmail: 'notification-email',
|
sendNotificationEmail: 'notification-email',
|
||||||
sendMonthlyEmail: 'announcement-email'
|
sendMonthlyEmail: 'announcement-email'
|
||||||
};
|
};
|
||||||
export default function userUpdateSaga(actions$, getState) {
|
|
||||||
|
export function updateUserEmailSaga(actions$, getState) {
|
||||||
|
return actions$
|
||||||
|
.filter(({ type }) => type === types.updateMyEmail)
|
||||||
|
.flatMap(({ payload: email }) => {
|
||||||
|
const {
|
||||||
|
app: { user: username, csrfToken: _csrf },
|
||||||
|
entities: { user: userMap }
|
||||||
|
} = getState();
|
||||||
|
const { email: oldEmail } = userMap[username] || {};
|
||||||
|
const body = { _csrf, email };
|
||||||
|
const optimisticUpdate$ = Observable.just(
|
||||||
|
updateUserEmail(username, email)
|
||||||
|
);
|
||||||
|
const ajaxUpdate$ = postJSON$('/update-my-email', body)
|
||||||
|
.map(({ message }) => makeToast({ message }))
|
||||||
|
.catch(doActionOnError(() => oldEmail ?
|
||||||
|
updateUserFlag(username, oldEmail) :
|
||||||
|
null
|
||||||
|
));
|
||||||
|
return Observable.merge(optimisticUpdate$, ajaxUpdate$);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserLangSaga(actions$, getState) {
|
||||||
|
const updateLang$ = actions$
|
||||||
|
.filter(({ type, payload }) => (
|
||||||
|
type === types.updateMyLang && !!langs[payload]
|
||||||
|
))
|
||||||
|
.map(({ payload }) => payload);
|
||||||
|
const ajaxUpdate$ = updateLang$
|
||||||
|
.flatMap(lang => {
|
||||||
|
const {
|
||||||
|
app: { user: username, csrfToken: _csrf },
|
||||||
|
entities: { user: userMap }
|
||||||
|
} = getState();
|
||||||
|
const { languageTag: oldLang } = userMap[username] || {};
|
||||||
|
const body = {
|
||||||
|
_csrf,
|
||||||
|
lang
|
||||||
|
};
|
||||||
|
return postJSON$('/update-my-lang', body)
|
||||||
|
.map(({ message }) => makeToast({ message }))
|
||||||
|
.catch(doActionOnError(() => {
|
||||||
|
return updateUserLang(username, oldLang);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
const optimistic$ = updateLang$
|
||||||
|
.map(lang => {
|
||||||
|
const { app: { user: username } } = getState();
|
||||||
|
return updateUserLang(username, lang);
|
||||||
|
});
|
||||||
|
return Observable.merge(ajaxUpdate$, optimistic$);
|
||||||
|
}
|
||||||
|
export function updateUserFlagSaga(actions$, getState) {
|
||||||
const toggleFlag$ = actions$
|
const toggleFlag$ = actions$
|
||||||
.filter(({ type, payload }) => type === types.toggleUserFlag && payload)
|
.filter(({ type, payload }) => type === types.toggleUserFlag && payload)
|
||||||
.map(({ payload }) => payload);
|
.map(({ payload }) => payload);
|
||||||
@ -34,7 +97,15 @@ export default function userUpdateSaga(actions$, getState) {
|
|||||||
}
|
}
|
||||||
return updateUserFlag(username, flag);
|
return updateUserFlag(username, flag);
|
||||||
})
|
})
|
||||||
.catch(createErrorObservable);
|
.catch(doActionOnError(() => {
|
||||||
|
return updateUserFlag(username, currentValue);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
return Observable.merge(optimistic$, serverUpdate$);
|
return Observable.merge(optimistic$, serverUpdate$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default combineSagas(
|
||||||
|
updateUserFlagSaga,
|
||||||
|
updateUserEmailSaga,
|
||||||
|
updateUserLangSaga
|
||||||
|
);
|
||||||
|
144
common/app/routes/settings/routes/update-email/Update-Email.jsx
Normal file
144
common/app/routes/settings/routes/update-email/Update-Email.jsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Button, HelpBlock, FormControl, FormGroup } from 'react-bootstrap';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import { reduxForm } from 'redux-form';
|
||||||
|
import { isEmail } from 'validator';
|
||||||
|
import { getValidationState } from '../../../../utils/form';
|
||||||
|
import { updateMyEmail } from '../../redux/actions';
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
updateMyEmail
|
||||||
|
};
|
||||||
|
const fields = [
|
||||||
|
'email',
|
||||||
|
'duplicate'
|
||||||
|
];
|
||||||
|
const validateFields = ({ email, duplicate }) => {
|
||||||
|
const errors = {};
|
||||||
|
if (!isEmail('' + email)) {
|
||||||
|
errors.email = 'This email is invalid.';
|
||||||
|
}
|
||||||
|
if (duplicate && email !== duplicate) {
|
||||||
|
errors.duplicate = 'This email does not match the one above.';
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
const {
|
||||||
|
app: { user: username },
|
||||||
|
entities: { user: userMap }
|
||||||
|
} = state;
|
||||||
|
const { email, emailVerified } = userMap[username] || {};
|
||||||
|
return {
|
||||||
|
initialValues: { email },
|
||||||
|
isEmailThere: !!email,
|
||||||
|
isVerified: !!emailVerified
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateEmail extends React.Component {
|
||||||
|
constructor(...props) {
|
||||||
|
super(...props);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
}
|
||||||
|
static displayName = 'UpdateEmail';
|
||||||
|
static propTypes = {
|
||||||
|
isEmailThere: PropTypes.bool,
|
||||||
|
isVerified: PropTypes.bool,
|
||||||
|
fields: PropTypes.object.isRequired,
|
||||||
|
submitting: PropTypes.bool,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
updateMyEmail: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.handleSubmit(({ email }) => this.props.updateMyEmail(email))(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isEmailThere,
|
||||||
|
isVerified,
|
||||||
|
fields: { email, duplicate },
|
||||||
|
submitting
|
||||||
|
} = this.props;
|
||||||
|
const buttonCopy = !isEmailThere || isVerified ?
|
||||||
|
'Update my Email' :
|
||||||
|
'Verify Email';
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className='text-center'>Update your email address here:</h2>
|
||||||
|
<form
|
||||||
|
name='update-email'
|
||||||
|
onSubmit={ this.handleSubmit }
|
||||||
|
>
|
||||||
|
<FormGroup
|
||||||
|
bsSize='lg'
|
||||||
|
controlId='email'
|
||||||
|
validationState={ getValidationState(email) }
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
autofocus={ true }
|
||||||
|
placeholder='Enter your new email'
|
||||||
|
type='email'
|
||||||
|
{ ...email }
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
!email.error ?
|
||||||
|
null :
|
||||||
|
<HelpBlock>{ email.error }</HelpBlock>
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
bsSize='lg'
|
||||||
|
controlId='duplicate'
|
||||||
|
validationState={ getValidationState(duplicate) }
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
placeholder='re-type your email address'
|
||||||
|
type='email'
|
||||||
|
{ ...duplicate }
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
!duplicate.error ?
|
||||||
|
null :
|
||||||
|
<HelpBlock>{ duplicate.error }</HelpBlock>
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
disabled={ submitting }
|
||||||
|
type='submit'
|
||||||
|
>
|
||||||
|
{ buttonCopy }
|
||||||
|
</Button>
|
||||||
|
<div className='button-spacer' />
|
||||||
|
<LinkContainer to='/settings'>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
>
|
||||||
|
Go back to Settings
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
</FormGroup>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reduxForm(
|
||||||
|
{
|
||||||
|
form: 'update-email',
|
||||||
|
fields,
|
||||||
|
validate: validateFields
|
||||||
|
},
|
||||||
|
mapStateToProps,
|
||||||
|
actions
|
||||||
|
)(UpdateEmail);
|
6
common/app/routes/settings/routes/update-email/index.js
Normal file
6
common/app/routes/settings/routes/update-email/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import UpdateEmail from './Update-Email.jsx';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
path: 'update-email',
|
||||||
|
component: UpdateEmail
|
||||||
|
};
|
9
common/app/utils/combine-sagas.js
Normal file
9
common/app/utils/combine-sagas.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Observable } from 'rx';
|
||||||
|
|
||||||
|
export default function combineSagas(...sagas) {
|
||||||
|
return (actions$, getState, deps) => {
|
||||||
|
return Observable.merge(
|
||||||
|
sagas.map(saga => saga(actions$, getState, deps))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -390,14 +390,14 @@ module.exports = function(User) {
|
|||||||
lastEmailSentAt.isBefore(fiveMinutesAgo) :
|
lastEmailSentAt.isBefore(fiveMinutesAgo) :
|
||||||
true;
|
true;
|
||||||
|
|
||||||
if (!isEmail(email)) {
|
if (!isEmail('' + email)) {
|
||||||
return Promise.reject(
|
return Observable.throw(
|
||||||
new Error('The submitted email not valid.')
|
new Error('The submitted email not valid.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// email is already associated and verified with this account
|
// email is already associated and verified with this account
|
||||||
if (ownEmail && this.emailVerified) {
|
if (ownEmail && this.emailVerified) {
|
||||||
return Promise.reject(new Error(
|
return Observable.throw(new Error(
|
||||||
`${email} is already associated with this account.`
|
`${email} is already associated with this account.`
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -410,13 +410,13 @@ module.exports = function(User) {
|
|||||||
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
|
`${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` :
|
||||||
'a few seconds';
|
'a few seconds';
|
||||||
|
|
||||||
return Promise.reject(new Error(
|
return Observable.throw(new Error(
|
||||||
`Please wait ${timeToWait} to resend email verification.`
|
`Please wait ${timeToWait} to resend email verification.`
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return User.doesExist(null, email)
|
return Observable.fromPromise(User.doesExist(null, email))
|
||||||
.then(exists => {
|
.flatMap(exists => {
|
||||||
// not associated with this account, but is associated with another
|
// not associated with this account, but is associated with another
|
||||||
if (!ownEmail && exists) {
|
if (!ownEmail && exists) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
@ -434,9 +434,10 @@ module.exports = function(User) {
|
|||||||
this.email = email;
|
this.email = email;
|
||||||
this.emailVerified = emailVerified;
|
this.emailVerified = emailVerified;
|
||||||
this.emailVerifyTTL = new Date();
|
this.emailVerifyTTL = new Date();
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.flatMap(() => {
|
.flatMap(() => {
|
||||||
var mailOptions = {
|
const mailOptions = {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
to: email,
|
to: email,
|
||||||
from: 'Team@freecodecamp.com',
|
from: 'Team@freecodecamp.com',
|
||||||
@ -459,42 +460,9 @@ module.exports = function(User) {
|
|||||||
.map(() => dedent`
|
.map(() => dedent`
|
||||||
Please check your email.
|
Please check your email.
|
||||||
We sent you a link that you can click to verify your email address.
|
We sent you a link that you can click to verify your email address.
|
||||||
`)
|
`);
|
||||||
.catch(error => {
|
|
||||||
debug(error);
|
|
||||||
return Observable.throw(
|
|
||||||
'Oops, something went wrong, please try again later.'
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toPromise();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
User.remoteMethod(
|
|
||||||
'updateEmail',
|
|
||||||
{
|
|
||||||
isStatic: false,
|
|
||||||
description: 'updates the email of the user object',
|
|
||||||
accepts: [
|
|
||||||
{
|
|
||||||
arg: 'email',
|
|
||||||
type: 'string',
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
returns: [
|
|
||||||
{
|
|
||||||
arg: 'message',
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
http: {
|
|
||||||
path: '/update-email',
|
|
||||||
verb: 'POST'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
User.giveBrowniePoints =
|
User.giveBrowniePoints =
|
||||||
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
|
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
|
||||||
const findUser = observeMethod(User, 'findOne');
|
const findUser = observeMethod(User, 'findOne');
|
||||||
|
3
server/boot/react.js
vendored
3
server/boot/react.js
vendored
@ -15,7 +15,8 @@ const routes = [
|
|||||||
'/challenges',
|
'/challenges',
|
||||||
'/challenges/*',
|
'/challenges/*',
|
||||||
'/map',
|
'/map',
|
||||||
'/settings'
|
'/settings',
|
||||||
|
'/settings/*'
|
||||||
];
|
];
|
||||||
|
|
||||||
const devRoutes = [];
|
const devRoutes = [];
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { ifNoUser401 } from '../utils/middleware';
|
import { ifNoUser401 } from '../utils/middleware';
|
||||||
|
import supportedLanguages from '../../common/utils/supported-languages.js';
|
||||||
|
|
||||||
export default function settingsController(app) {
|
export default function settingsController(app) {
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
@ -15,6 +16,39 @@ export default function settingsController(app) {
|
|||||||
next
|
next
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateMyEmail(req, res, next) {
|
||||||
|
const { user, body: { email } } = req;
|
||||||
|
return user.updateEmail(email)
|
||||||
|
.subscribe(
|
||||||
|
(message) => res.json({ message }),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMyLang(req, res, next) {
|
||||||
|
const { user, body: { lang } = {} } = req;
|
||||||
|
const langName = supportedLanguages[lang];
|
||||||
|
const update = { languageTag: lang };
|
||||||
|
if (!supportedLanguages[lang]) {
|
||||||
|
return res.json({
|
||||||
|
message: `${lang} is currently unsupported`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user.langaugeTag === lang) {
|
||||||
|
return res.json({
|
||||||
|
message: `Your language is already set to ${langName}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return user.update$(update)
|
||||||
|
.subscribe(
|
||||||
|
() => res.json({
|
||||||
|
message: `Your language has been updated to '${langName}'`
|
||||||
|
}),
|
||||||
|
next
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/toggle-lockdown',
|
'/toggle-lockdown',
|
||||||
toggleUserFlag('isLocked')
|
toggleUserFlag('isLocked')
|
||||||
@ -34,5 +68,15 @@ export default function settingsController(app) {
|
|||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
toggleUserFlag('sendQuincyEmail')
|
toggleUserFlag('sendQuincyEmail')
|
||||||
);
|
);
|
||||||
|
api.post(
|
||||||
|
'/update-my-email',
|
||||||
|
ifNoUser401,
|
||||||
|
updateMyEmail
|
||||||
|
);
|
||||||
|
api.post(
|
||||||
|
'/update-my-lang',
|
||||||
|
ifNoUser401,
|
||||||
|
updateMyLang
|
||||||
|
);
|
||||||
app.use(api);
|
app.use(api);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user