diff --git a/api-server/server/boot/restApi.js b/api-server/server/boot/restApi.js
index e81c692006..d12911326a 100644
--- a/api-server/server/boot/restApi.js
+++ b/api-server/server/boot/restApi.js
@@ -1,4 +1,6 @@
module.exports = function mountRestApi(app) {
- var restApiRoot = app.get('restApiRoot');
- app.use(restApiRoot, app.loopback.rest());
+ const restApi = app.loopback.rest();
+ const restApiRoot = app.get('restApiRoot');
+ app.use(restApiRoot, restApi);
+ app.use(`/internal${restApiRoot}`, restApi);
};
diff --git a/api-server/server/boot/settings.js b/api-server/server/boot/settings.js
index 4de5360cbd..3f0c25a431 100644
--- a/api-server/server/boot/settings.js
+++ b/api-server/server/boot/settings.js
@@ -5,167 +5,8 @@ import { alertTypes } from '../../common/utils/flash.js';
export default function settingsController(app) {
const api = app.loopback.Router();
- const toggleUserFlag = (flag, req, res, next) => {
- const { user } = req;
- const currentValue = user[flag];
- return user.update$({ [flag]: !currentValue }).subscribe(
- () =>
- res.status(200).json({
- flag,
- value: !currentValue
- }),
- next
- );
- };
- function refetchCompletedChallenges(req, res, next) {
- const { user } = req;
- return user
- .requestCompletedChallenges()
- .subscribe(completedChallenges => res.json({ completedChallenges }), next);
- }
-
- const updateMyEmailValidators = [
- check('email')
- .isEmail()
- .withMessage('Email format is invalid.')
- ];
-
- function updateMyEmail(req, res, next) {
- const {
- user,
- body: { email }
- } = req;
- return user
- .requestUpdateEmail(email)
- .subscribe(message => res.json({ message }), next);
- }
-
- const updateMyCurrentChallengeValidators = [
- check('currentChallengeId')
- .isMongoId()
- .withMessage('currentChallengeId is not a valid challenge ID')
- ];
-
- function updateMyCurrentChallenge(req, res, next) {
- const {
- user,
- body: { currentChallengeId }
- } = req;
- return user.update$({ currentChallengeId }).subscribe(
- () =>
- res.json({
- message: `your current challenge has been updated to ${currentChallengeId}`
- }),
- next
- );
- }
-
- const updateMyThemeValidators = [
- check('theme')
- .isIn(Object.keys(themes))
- .withMessage('Theme is invalid.')
- ];
-
- function updateMyTheme(req, res, next) {
- const {
- body: { theme }
- } = req;
- if (req.user.theme === theme) {
- return res.sendFlash(alertTypes.info, 'Theme already set');
- }
- return req.user
- .updateTheme(theme)
- .then(
- () => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
- next
- );
- }
-
- function updateFlags(req, res, next) {
- const {
- user,
- body: { values }
- } = req;
- const keys = Object.keys(values);
- if (keys.length === 1 && typeof keys[0] === 'boolean') {
- return toggleUserFlag(keys[0], req, res, next);
- }
- return user
- .requestUpdateFlags(values)
- .subscribe(message => res.json({ message }), next);
- }
-
- function updateMyPortfolio(req, res, next) {
- const {
- user,
- body: { portfolio }
- } = req;
- // if we only have one key, it should be the id
- // user cannot send only one key to this route
- // other than to remove a portfolio item
- const requestDelete = Object.keys(portfolio).length === 1;
- return user
- .updateMyPortfolio(portfolio, requestDelete)
- .subscribe(message => res.json({ message }), next);
- }
-
- function updateMyProfileUI(req, res, next) {
- const {
- user,
- body: { profileUI }
- } = req;
- return user
- .updateMyProfileUI(profileUI)
- .subscribe(message => res.json({ message }), next);
- }
-
- function updateMyProjects(req, res, next) {
- const {
- user,
- body: { projects: project }
- } = req;
- return user
- .updateMyProjects(project)
- .subscribe(message => res.json({ message }), next);
- }
-
- function updateMyUsername(req, res, next) {
- const {
- user,
- body: { username }
- } = req;
- return user
- .updateMyUsername(username)
- .subscribe(message => res.json({ message }), next);
- }
-
- const updatePrivacyTerms = (req, res) => {
- const {
- user,
- body: { quincyEmails }
- } = req;
- const update = {
- acceptedPrivacyTerms: true,
- sendQuincyEmail: !!quincyEmails
- };
- return user.updateAttributes(update, err => {
- if (err) {
- return res.status(500).json({
- type: 'warning',
- message:
- 'Something went wrong updating your preferences. ' +
- 'Please try again.'
- });
- }
- return res.status(200).json({
- type: 'success',
- message:
- 'We have updated your preferences. ' +
- 'You can now continue using freeCodeCamp.'
- });
- });
- };
+ const updateMyUsername = createUpdateMyUsername(app);
api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms);
@@ -206,9 +47,192 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger),
updateMyTheme
);
- api.post('/update-my-username', ifNoUser401, updateMyUsername);
+ api.put('/update-my-username', ifNoUser401, updateMyUsername);
- app.use('/external', api);
app.use('/internal', api);
app.use(api);
}
+
+const standardErrorMessage = {
+ type: 'danger',
+ message:
+ 'Something went wrong updating your account. Please check and try again'
+};
+
+const toggleUserFlag = (flag, req, res, next) => {
+ const { user } = req;
+ const currentValue = user[flag];
+ return user.update$({ [flag]: !currentValue }).subscribe(
+ () =>
+ res.status(200).json({
+ flag,
+ value: !currentValue
+ }),
+ next
+ );
+};
+
+function refetchCompletedChallenges(req, res, next) {
+ const { user } = req;
+ return user
+ .requestCompletedChallenges()
+ .subscribe(completedChallenges => res.json({ completedChallenges }), next);
+}
+
+const updateMyEmailValidators = [
+ check('email')
+ .isEmail()
+ .withMessage('Email format is invalid.')
+];
+
+function updateMyEmail(req, res, next) {
+ const {
+ user,
+ body: { email }
+ } = req;
+ return user
+ .requestUpdateEmail(email)
+ .subscribe(message => res.json({ message }), next);
+}
+
+const updateMyCurrentChallengeValidators = [
+ check('currentChallengeId')
+ .isMongoId()
+ .withMessage('currentChallengeId is not a valid challenge ID')
+];
+
+function updateMyCurrentChallenge(req, res, next) {
+ const {
+ user,
+ body: { currentChallengeId }
+ } = req;
+ return user
+ .update$({ currentChallengeId })
+ .subscribe(() => res.status(200), next);
+}
+
+const updateMyThemeValidators = [
+ check('theme')
+ .isIn(Object.keys(themes))
+ .withMessage('Theme is invalid.')
+];
+
+function updateMyTheme(req, res, next) {
+ const {
+ body: { theme }
+ } = req;
+ if (req.user.theme === theme) {
+ return res.sendFlash(alertTypes.info, 'Theme already set');
+ }
+ return req.user
+ .updateTheme(theme)
+ .then(
+ () => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
+ next
+ );
+}
+
+function updateFlags(req, res, next) {
+ const {
+ user,
+ body: { values }
+ } = req;
+ const keys = Object.keys(values);
+ if (keys.length === 1 && typeof keys[0] === 'boolean') {
+ return toggleUserFlag(keys[0], req, res, next);
+ }
+ return user
+ .requestUpdateFlags(values)
+ .subscribe(message => res.json({ message }), next);
+}
+
+function updateMyPortfolio(req, res, next) {
+ const {
+ user,
+ body: { portfolio }
+ } = req;
+ // if we only have one key, it should be the id
+ // user cannot send only one key to this route
+ // other than to remove a portfolio item
+ const requestDelete = Object.keys(portfolio).length === 1;
+ return user
+ .updateMyPortfolio(portfolio, requestDelete)
+ .subscribe(message => res.json({ message }), next);
+}
+
+function updateMyProfileUI(req, res, next) {
+ const {
+ user,
+ body: { profileUI }
+ } = req;
+ return user
+ .updateMyProfileUI(profileUI)
+ .subscribe(message => res.json({ message }), next);
+}
+
+function updateMyProjects(req, res, next) {
+ const {
+ user,
+ body: { projects: project }
+ } = req;
+ return user
+ .updateMyProjects(project)
+ .subscribe(message => res.json({ message }), next);
+}
+
+function createUpdateMyUsername(app) {
+ const { User } = app.models;
+ return async function updateMyUsername(req, res, next) {
+ const {
+ user,
+ body: { username }
+ } = req;
+ if (username === user.username) {
+ return res.json({
+ type: 'info',
+ message: 'Username is already associated with this account'
+ });
+ }
+ const exists = await User.doesExist(username);
+
+ if (exists) {
+ return res.json({
+ type: 'info',
+ message: 'Username is already associated with a different account'
+ });
+ }
+
+ return user.updateAttribute('username', username, err => {
+ if (err) {
+ res.status(500).json(standardErrorMessage);
+ return next(err);
+ }
+ return res.status(200).json({
+ type: 'success',
+ message: `We have updated your username to ${username}`
+ });
+ });
+ };
+}
+
+const updatePrivacyTerms = (req, res) => {
+ const {
+ user,
+ body: { quincyEmails }
+ } = req;
+ const update = {
+ acceptedPrivacyTerms: true,
+ sendQuincyEmail: !!quincyEmails
+ };
+ return user.updateAttributes(update, err => {
+ if (err) {
+ return res.status(500).json(standardErrorMessage);
+ }
+ return res.status(200).json({
+ type: 'success',
+ message:
+ 'We have updated your preferences. ' +
+ 'You can now continue using freeCodeCamp.'
+ });
+ });
+};
diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js
index abc6a888cd..d6c2f85597 100644
--- a/client/src/client-only-routes/ShowSettings.js
+++ b/client/src/client-only-routes/ShowSettings.js
@@ -92,7 +92,7 @@ function ShowSettings(props) {
{`Account Settings for ${username}`}
- {/*
-
+ {/*
diff --git a/client/src/components/global.css b/client/src/components/global.css
index 21caf596cf..04c52aa971 100644
--- a/client/src/components/global.css
+++ b/client/src/components/global.css
@@ -22,4 +22,10 @@ h6 {
.green-text {
color: #006400;
+}
+
+.btn-invert {
+ background-color: #fff;
+ color: #006400;
+
}
\ No newline at end of file
diff --git a/client/src/components/settings/About.js b/client/src/components/settings/About.js
new file mode 100644
index 0000000000..21d3e4564b
--- /dev/null
+++ b/client/src/components/settings/About.js
@@ -0,0 +1,227 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import {
+ Nav,
+ NavItem,
+ FormGroup,
+ ControlLabel,
+ FormControl
+} from '@freecodecamp/react-bootstrap';
+
+import { FullWidthRow, Spacer } from '../helpers';
+import ThemeSettings from './Theme';
+import Camper from './Camper';
+import UsernameSettings from './Username';
+import BlockSaveButton from '../helpers/form/BlockSaveButton';
+import BlockSaveWrapper from '../helpers/form/BlockSaveWrapper';
+
+const mapStateToProps = () => ({});
+
+const mapDispatchToProps = dispatch => bindActionCreators({}, dispatch);
+
+const propTypes = {
+ about: PropTypes.string,
+ currentTheme: PropTypes.string,
+ location: PropTypes.string,
+ name: PropTypes.string,
+ picture: PropTypes.string,
+ points: PropTypes.number,
+ username: PropTypes.string
+};
+
+class AboutSettings extends Component {
+ constructor(props) {
+ super(props);
+
+ const { name = '', location = '', picture = '', about = '' } = props;
+
+ this.state = {
+ view: 'edit',
+ formValues: {
+ name,
+ location,
+ picture,
+ about
+ },
+ isFormPristine: true
+ };
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleTabSelect = this.handleTabSelect.bind(this);
+ this.renderEdit = this.renderEdit.bind(this);
+ this.renderPreview = this.renderPreview.bind(this);
+ this.show = {
+ edit: this.renderEdit,
+ preview: this.renderPreview
+ };
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ const { formValues } = this.state;
+ console.log(formValues)
+ }
+
+ handleNameChange = e => {
+ const value = e.target.value.slice(0);
+ return this.setState(state => ({
+ formValues: {
+ ...state.formValues,
+ name: value
+ }
+ }));
+ };
+
+ handleLocationChange = e => {
+ const value = e.target.value.slice(0);
+ return this.setState(state => ({
+ formValues: {
+ ...state.formValues,
+ location: value
+ }
+ }));
+ };
+
+ handlePictureChange = e => {
+ const value = e.target.value.slice(0);
+ return this.setState(state => ({
+ formValues: {
+ ...state.formValues,
+ picture: value
+ }
+ }));
+ };
+
+ handleAboutChange = e => {
+ const value = e.target.value.slice(0);
+ return this.setState(state => ({
+ formValues: {
+ ...state.formValues,
+ about: value
+ }
+ }));
+ };
+
+ handleTabSelect(key) {
+ return this.setState(state => ({
+ ...state,
+ view: key
+ }));
+ }
+
+ renderEdit() {
+ const {
+ formValues: { name, location, picture, about }
+ } = this.state;
+ return (
+
+
+
+ Name
+
+
+
+
+
+ Location
+
+
+
+
+
+ Picture
+
+
+
+
+
+ About
+
+
+
+
+ );
+ }
+
+ renderPreview() {
+ const { about, picture, points, username, name, location } = this.props;
+ return (
+
+ );
+ }
+
+ render() {
+ const { currentTheme, username } = this.props;
+ const { view, isFormPristine } = this.state;
+
+ const toggleTheme = () => {};
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+AboutSettings.displayName = 'AboutSettings';
+AboutSettings.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(AboutSettings);
diff --git a/client/src/components/settings/Camper.js b/client/src/components/settings/Camper.js
new file mode 100644
index 0000000000..536e4166c3
--- /dev/null
+++ b/client/src/components/settings/Camper.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+function CamperSettings() {
+ return (CamperSettings
);
+}
+
+CamperSettings.displayName = 'CamperSettings';
+
+export default CamperSettings;
diff --git a/client/src/components/settings/Theme.js b/client/src/components/settings/Theme.js
new file mode 100644
index 0000000000..83bb323ecd
--- /dev/null
+++ b/client/src/components/settings/Theme.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+function ThemeSettings() {
+ return (ThemeSettings
);
+}
+
+ThemeSettings.displayName = 'ThemeSettings';
+
+export default ThemeSettings;
diff --git a/client/src/components/settings/Username.js b/client/src/components/settings/Username.js
new file mode 100644
index 0000000000..5d0b2880ca
--- /dev/null
+++ b/client/src/components/settings/Username.js
@@ -0,0 +1,209 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import {
+ ControlLabel,
+ FormControl,
+ Alert,
+ FormGroup
+} from '@freecodecamp/react-bootstrap';
+import isAscii from 'validator/lib/isAscii';
+
+import {
+ validateUsername,
+ usernameValidationSelector,
+ submitNewUsername
+} from '../../redux/settings';
+import BlockSaveButton from '../helpers/form/BlockSaveButton';
+import FullWidthRow from '../helpers/FullWidthRow';
+
+const propTypes = {
+ isValidUsername: PropTypes.bool,
+ submitNewUsername: PropTypes.func.isRequired,
+ username: PropTypes.string,
+ validateUsername: PropTypes.func.isRequired,
+ validating: PropTypes.bool
+};
+
+const mapStateToProps = createSelector(
+ usernameValidationSelector,
+ ({ isValidUsername, fetchState }) => ({
+ isValidUsername,
+ validating: fetchState.pending
+ })
+);
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators(
+ {
+ submitNewUsername,
+ validateUsername
+ },
+ dispatch
+ );
+
+const invalidCharsRE = /[/\s?:@=&"'<>#%{}|\\^~[\]`,.;!*()$]/;
+const invlaidCharError = {
+ valid: false,
+ error: 'Username contains invalid characters'
+};
+const valididationSuccess = { valid: true, error: null };
+const usernameTooShort = { valid: false, error: 'Username is too short' };
+
+class UsernameSettings extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isFormPristine: true,
+ formValue: props.username,
+ characterValidation: { valid: false, error: null },
+ sumbitClicked: false
+ };
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ this.validateFormInput = this.validateFormInput.bind(this);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ const { username: prevUsername } = prevProps;
+ const { formValue: prevFormValue } = prevState;
+ const { username } = this.props;
+ const { formValue } = this.state;
+ if (prevUsername !== username && prevFormValue === formValue) {
+ /* eslint-disable-next-line react/no-did-update-set-state */
+ return this.setState({
+ isFormPristine: username === formValue,
+ sumbitClicked: false
+ });
+ }
+ return null;
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ const { submitNewUsername } = this.props;
+ const {
+ formValue,
+ characterValidation: { valid }
+ } = this.state;
+
+ return this.setState(
+ { sumbitClicked: true },
+ () => (valid ? submitNewUsername(formValue) : null)
+ );
+ }
+
+ handleChange(e) {
+ e.preventDefault();
+ const { username, validateUsername } = this.props;
+ const newValue = e.target.value.toLowerCase();
+ return this.setState(
+ {
+ formValue: newValue,
+ isFormPristine: username === newValue,
+ characterValidation: this.validateFormInput(newValue)
+ },
+ () =>
+ this.state.isFormPristine || this.state.characterValidation.error
+ ? null
+ : validateUsername(this.state.formValue)
+ );
+ }
+
+ validateFormInput(formValue) {
+ if (formValue.length < 3) {
+ return usernameTooShort;
+ }
+
+ if (!isAscii(formValue)) {
+ return invlaidCharError;
+ }
+ if (invalidCharsRE.test(formValue)) {
+ return invlaidCharError;
+ }
+ return valididationSuccess;
+ }
+
+ renderAlerts(validating, error, isValidUsername) {
+ if (!validating && error) {
+ return (
+
+ {error}
+
+ );
+ }
+ if (!validating && !isValidUsername) {
+ console.log(this.props, this.state);
+ return (
+
+ Username not available
+
+ );
+ }
+ if (validating) {
+ return (
+
+ Validating username
+
+ );
+ }
+ if (!validating && isValidUsername) {
+ return (
+
+ Username is available
+
+ );
+ }
+ return null;
+ }
+
+ render() {
+ const {
+ isFormPristine,
+ formValue,
+ characterValidation: { valid, error },
+ sumbitClicked
+ } = this.state;
+ const { isValidUsername, validating } = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
+
+UsernameSettings.displayName = 'UsernameSettings';
+UsernameSettings.propTypes = propTypes;
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(UsernameSettings);
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index 7f7b99a2b7..3fc20bd74e 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -8,6 +8,8 @@ import { createReportUserSaga } from './report-user-saga';
import { createShowCertSaga } from './show-cert-saga';
import { createUpdateMyEmailSaga } from './update-email-saga';
+import { types as settingsTypes } from './settings';
+
const ns = 'app';
const defaultFetchState = {
@@ -74,6 +76,9 @@ export const updateMyEmailError = createAction(types.updateMyEmailError);
export const isSignedInSelector = state => !!Object.keys(state[ns].user).length;
+export const signInLoadingSelector = state =>
+ userFetchStateSelector(state).pending;
+
export const showCertSelector = state => state[ns].showCert;
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
@@ -83,7 +88,11 @@ export const userByNameSelector = username => state => {
};
export const userFetchStateSelector = state => state[ns].userFetchState;
export const usernameSelector = state => state[ns].appUsername;
-export const userSelector = state => state[ns].user;
+export const userSelector = state => {
+ const username = usernameSelector(state);
+
+ return state[ns].user[username] || {};
+};
export const reducer = handleActions(
{
@@ -93,7 +102,10 @@ export const reducer = handleActions(
}),
[types.fetchUserComplete]: (state, { payload: { user, username } }) => ({
...state,
- user,
+ user: {
+ ...state.user,
+ [username]: user
+ },
appUsername: username,
userFetchState: {
pending: false,
@@ -135,7 +147,20 @@ export const reducer = handleActions(
errored: true,
error: payload
}
- })
+ }),
+ [settingsTypes.submitNewUsernameComplete]: (state, { payload }) =>
+ payload
+ ? {
+ ...state,
+ user: {
+ ...state.user,
+ [state.appUsername]: {
+ ...state.user[state.appUsername],
+ username: payload
+ }
+ }
+ }
+ : state
},
initialState
);
diff --git a/client/src/redux/rootReducer.js b/client/src/redux/rootReducer.js
index fb3e1b9832..f1ba6b2071 100644
--- a/client/src/redux/rootReducer.js
+++ b/client/src/redux/rootReducer.js
@@ -2,8 +2,10 @@ import { combineReducers } from 'redux';
import { reducer as app } from './';
import { reducer as flash } from '../components/Flash/redux';
+import { reducer as settings } from './settings';
export default combineReducers({
app,
- flash
+ flash,
+ settings
});
diff --git a/client/src/redux/rootSaga.js b/client/src/redux/rootSaga.js
index a10b2e35f1..d5965a2a40 100644
--- a/client/src/redux/rootSaga.js
+++ b/client/src/redux/rootSaga.js
@@ -1,7 +1,8 @@
import { all } from 'redux-saga/effects';
import { sagas as appSagas } from './';
+import { sagas as settingsSagas } from './settings';
export default function* rootSaga() {
- yield all([...appSagas]);
+ yield all([...appSagas, ...settingsSagas]);
}
diff --git a/client/src/redux/settings/index.js b/client/src/redux/settings/index.js
new file mode 100644
index 0000000000..be03c2bd85
--- /dev/null
+++ b/client/src/redux/settings/index.js
@@ -0,0 +1,73 @@
+import { createAction, handleActions } from 'redux-actions';
+
+import { createTypes, createAsyncTypes } from '../../utils/createTypes';
+import { createSettingsSagas } from './settings-sagas';
+
+const ns = 'settings';
+
+const defaultFetchState = {
+ pending: false,
+ complete: false,
+ errored: false,
+ error: null
+};
+
+const initialState = {
+ usernameValidation: {
+ isValidUsername: false,
+ fetchState: { ...defaultFetchState }
+ }
+};
+
+export const types = createTypes(
+ [
+ ...createAsyncTypes('validateUsername'),
+ ...createAsyncTypes('submitNewUsername')
+ ],
+ ns
+);
+
+export const sagas = [...createSettingsSagas(types)];
+
+export const submitNewUsername = createAction(types.submitNewUsername);
+export const submitNewUsernameComplete = createAction(
+ types.submitNewUsernameComplete,
+ ({ type, username }) => (type === 'success' ? username : null)
+);
+export const submitNewUsernameError = createAction(
+ types.submitNewUsernameError
+);
+
+export const validateUsername = createAction(types.validateUsername);
+export const validateUsernameComplete = createAction(
+ types.validateUsernameComplete
+);
+export const validateUsernameError = createAction(types.validateUsernameError);
+
+export const usernameValidationSelector = state => state[ns].usernameValidation;
+
+export const reducer = handleActions(
+ {
+ [types.submitNewUsernameComplete]: state => ({
+ ...state,
+ usernameValidation: { ...initialState.usernameValidation }
+ }),
+ [types.validateUsername]: state => ({
+ ...state,
+ usernameValidation: {
+ ...state.usernameValidation,
+ isValidUsername: false,
+ fetchState: { ...defaultFetchState, pending: true }
+ }
+ }),
+ [types.validateUsernameComplete]: (state, { payload }) => ({
+ ...state,
+ usernameValidation: {
+ ...state.usernameValidation,
+ isValidUsername: !payload,
+ fetchState: { ...defaultFetchState, pending: false, complete: true }
+ }
+ })
+ },
+ initialState
+);
diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js
new file mode 100644
index 0000000000..d61d83560b
--- /dev/null
+++ b/client/src/redux/settings/settings-sagas.js
@@ -0,0 +1,41 @@
+import { delay } from 'redux-saga';
+import { call, put, takeLatest } from 'redux-saga/effects';
+
+import {
+ validateUsernameComplete,
+ validateUsernameError,
+ submitNewUsernameComplete,
+ submitNewUsernameError
+} from './';
+
+import { getUsernameExists, putUpdateMyUsername } from '../../utils/ajax';
+import { createFlashMessage } from '../../components/Flash/redux';
+
+function* validateUsernameSaga({ payload }) {
+ try {
+ yield delay(500);
+ const {
+ data: { exists }
+ } = yield call(getUsernameExists, payload);
+ yield put(validateUsernameComplete(exists));
+ } catch (e) {
+ yield put(validateUsernameError(e));
+ }
+}
+
+function* submitNEwUswernameSaga({ payload: username }) {
+ try {
+ const { data: response } = yield call(putUpdateMyUsername, username);
+ yield put(submitNewUsernameComplete({...response, username}));
+ yield put(createFlashMessage(response));
+ } catch (e) {
+ yield put(submitNewUsernameError(e));
+ }
+}
+
+export function createSettingsSagas(types) {
+ return [
+ takeLatest(types.validateUsername, validateUsernameSaga),
+ takeLatest(types.submitNewUsername, submitNEwUswernameSaga)
+ ];
+}
diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js
index 3f9249b374..b7091a1427 100644
--- a/client/src/utils/ajax.js
+++ b/client/src/utils/ajax.js
@@ -28,6 +28,10 @@ export function getShowCert(username, cert) {
return get(`/certificate/showCert/${username}/${cert}`);
}
+export function getUsernameExists(username) {
+ return get(`/api/users/exists?username=${username}`);
+}
+
/** POST **/
export function postReportUser(body) {
@@ -36,6 +40,10 @@ export function postReportUser(body) {
/** PUT **/
+export function putUpdateMyUsername(username) {
+ return put('/update-my-username', { username });
+}
+
export function putUserAcceptsTerms(quincyEmails) {
return put('/update-privacy-terms', { quincyEmails });
}