feat(username): Add Username updating
This commit is contained in:
@ -1,4 +1,6 @@
|
|||||||
module.exports = function mountRestApi(app) {
|
module.exports = function mountRestApi(app) {
|
||||||
var restApiRoot = app.get('restApiRoot');
|
const restApi = app.loopback.rest();
|
||||||
app.use(restApiRoot, app.loopback.rest());
|
const restApiRoot = app.get('restApiRoot');
|
||||||
|
app.use(restApiRoot, restApi);
|
||||||
|
app.use(`/internal${restApiRoot}`, restApi);
|
||||||
};
|
};
|
||||||
|
@ -5,167 +5,8 @@ import { alertTypes } from '../../common/utils/flash.js';
|
|||||||
|
|
||||||
export default function settingsController(app) {
|
export default function settingsController(app) {
|
||||||
const api = app.loopback.Router();
|
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 updateMyUsername = createUpdateMyUsername(app);
|
||||||
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.'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms);
|
api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms);
|
||||||
|
|
||||||
@ -206,9 +47,192 @@ export default function settingsController(app) {
|
|||||||
createValidatorErrorHandler(alertTypes.danger),
|
createValidatorErrorHandler(alertTypes.danger),
|
||||||
updateMyTheme
|
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('/internal', api);
|
||||||
app.use(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.'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -92,7 +92,7 @@ function ShowSettings(props) {
|
|||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<h1 className='text-center'>{`Account Settings for ${username}`}</h1>
|
<h1 className='text-center'>{`Account Settings for ${username}`}</h1>
|
||||||
{/* <About
|
<About
|
||||||
about={about}
|
about={about}
|
||||||
currentTheme={theme}
|
currentTheme={theme}
|
||||||
location={location}
|
location={location}
|
||||||
@ -102,7 +102,7 @@ function ShowSettings(props) {
|
|||||||
username={username}
|
username={username}
|
||||||
/>
|
/>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<PrivacySettings />
|
{/* <PrivacySettings />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<EmailSettings />
|
<EmailSettings />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
@ -22,4 +22,10 @@ h6 {
|
|||||||
|
|
||||||
.green-text {
|
.green-text {
|
||||||
color: #006400;
|
color: #006400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-invert {
|
||||||
|
background-color: #fff;
|
||||||
|
color: #006400;
|
||||||
|
|
||||||
}
|
}
|
227
client/src/components/settings/About.js
Normal file
227
client/src/components/settings/About.js
Normal file
@ -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 (
|
||||||
|
<Fragment>
|
||||||
|
<FormGroup controlId='about-name'>
|
||||||
|
<ControlLabel>
|
||||||
|
<strong>Name</strong>
|
||||||
|
</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={this.handleNameChange}
|
||||||
|
type='text'
|
||||||
|
value={name}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup controlId='about-location'>
|
||||||
|
<ControlLabel>
|
||||||
|
<strong>Location</strong>
|
||||||
|
</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={this.handleLocationChange}
|
||||||
|
type='text'
|
||||||
|
value={location}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup controlId='about-picture'>
|
||||||
|
<ControlLabel>
|
||||||
|
<strong>Picture</strong>
|
||||||
|
</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
onChange={this.handlePictureChange}
|
||||||
|
required={true}
|
||||||
|
type='url'
|
||||||
|
value={picture}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup controlId='about-about'>
|
||||||
|
<ControlLabel>
|
||||||
|
<strong>About</strong>
|
||||||
|
</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
componentClass='textarea'
|
||||||
|
onChange={this.handleAboutChange}
|
||||||
|
value={about}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreview() {
|
||||||
|
const { about, picture, points, username, name, location } = this.props;
|
||||||
|
return (
|
||||||
|
<Camper
|
||||||
|
about={about}
|
||||||
|
location={location}
|
||||||
|
name={name}
|
||||||
|
picture={picture}
|
||||||
|
points={points}
|
||||||
|
username={username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { currentTheme, username } = this.props;
|
||||||
|
const { view, isFormPristine } = this.state;
|
||||||
|
|
||||||
|
const toggleTheme = () => {};
|
||||||
|
return (
|
||||||
|
<div className='about-settings'>
|
||||||
|
<UsernameSettings username={username} />
|
||||||
|
<FullWidthRow>
|
||||||
|
<Nav
|
||||||
|
activeKey={view}
|
||||||
|
bsStyle='tabs'
|
||||||
|
className='edit-preview-tabs'
|
||||||
|
onSelect={k => this.handleTabSelect(k)}
|
||||||
|
>
|
||||||
|
<NavItem eventKey='edit' title='Edit Bio'>
|
||||||
|
Edit Bio
|
||||||
|
</NavItem>
|
||||||
|
<NavItem eventKey='preview' title='Preview Bio'>
|
||||||
|
Preview Bio
|
||||||
|
</NavItem>
|
||||||
|
</Nav>
|
||||||
|
</FullWidthRow>
|
||||||
|
<br />
|
||||||
|
<FullWidthRow>
|
||||||
|
<form id='camper-identity' onSubmit={this.handleSubmit}>
|
||||||
|
{this.show[view]()}
|
||||||
|
<BlockSaveButton disabled={isFormPristine} />
|
||||||
|
</form>
|
||||||
|
</FullWidthRow>
|
||||||
|
<Spacer />
|
||||||
|
<FullWidthRow>
|
||||||
|
<ThemeSettings
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
toggleNightMode={toggleTheme}
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AboutSettings.displayName = 'AboutSettings';
|
||||||
|
AboutSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AboutSettings);
|
9
client/src/components/settings/Camper.js
Normal file
9
client/src/components/settings/Camper.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function CamperSettings() {
|
||||||
|
return (<h1>CamperSettings</h1>);
|
||||||
|
}
|
||||||
|
|
||||||
|
CamperSettings.displayName = 'CamperSettings';
|
||||||
|
|
||||||
|
export default CamperSettings;
|
9
client/src/components/settings/Theme.js
Normal file
9
client/src/components/settings/Theme.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ThemeSettings() {
|
||||||
|
return (<h1>ThemeSettings</h1>);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeSettings.displayName = 'ThemeSettings';
|
||||||
|
|
||||||
|
export default ThemeSettings;
|
209
client/src/components/settings/Username.js
Normal file
209
client/src/components/settings/Username.js
Normal file
@ -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 (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='danger'>{error}</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!validating && !isValidUsername) {
|
||||||
|
console.log(this.props, this.state);
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='warning'>Username not available</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (validating) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='info'>Validating username</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!validating && isValidUsername) {
|
||||||
|
return (
|
||||||
|
<FullWidthRow>
|
||||||
|
<Alert bsStyle='success'>Username is available</Alert>
|
||||||
|
</FullWidthRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFormPristine,
|
||||||
|
formValue,
|
||||||
|
characterValidation: { valid, error },
|
||||||
|
sumbitClicked
|
||||||
|
} = this.state;
|
||||||
|
const { isValidUsername, validating } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<form id='usernameSettings' onSubmit={this.handleSubmit}>
|
||||||
|
<FullWidthRow>
|
||||||
|
<FormGroup>
|
||||||
|
<ControlLabel htmlFor='username-settings'>
|
||||||
|
<strong>Username</strong>
|
||||||
|
</ControlLabel>
|
||||||
|
<FormControl
|
||||||
|
name='username-settings'
|
||||||
|
onChange={this.handleChange}
|
||||||
|
value={formValue}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</FullWidthRow>
|
||||||
|
{!isFormPristine &&
|
||||||
|
this.renderAlerts(validating, error, isValidUsername)}
|
||||||
|
<FullWidthRow>
|
||||||
|
<BlockSaveButton
|
||||||
|
disabled={
|
||||||
|
!(isValidUsername && valid && !isFormPristine) || sumbitClicked
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
</form>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UsernameSettings.displayName = 'UsernameSettings';
|
||||||
|
UsernameSettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(UsernameSettings);
|
@ -8,6 +8,8 @@ import { createReportUserSaga } from './report-user-saga';
|
|||||||
import { createShowCertSaga } from './show-cert-saga';
|
import { createShowCertSaga } from './show-cert-saga';
|
||||||
import { createUpdateMyEmailSaga } from './update-email-saga';
|
import { createUpdateMyEmailSaga } from './update-email-saga';
|
||||||
|
|
||||||
|
import { types as settingsTypes } from './settings';
|
||||||
|
|
||||||
const ns = 'app';
|
const ns = 'app';
|
||||||
|
|
||||||
const defaultFetchState = {
|
const defaultFetchState = {
|
||||||
@ -74,6 +76,9 @@ export const updateMyEmailError = createAction(types.updateMyEmailError);
|
|||||||
|
|
||||||
export const isSignedInSelector = state => !!Object.keys(state[ns].user).length;
|
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 showCertSelector = state => state[ns].showCert;
|
||||||
export const showCertFetchStateSelector = state => state[ns].showCertFetchState;
|
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 userFetchStateSelector = state => state[ns].userFetchState;
|
||||||
export const usernameSelector = state => state[ns].appUsername;
|
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(
|
export const reducer = handleActions(
|
||||||
{
|
{
|
||||||
@ -93,7 +102,10 @@ export const reducer = handleActions(
|
|||||||
}),
|
}),
|
||||||
[types.fetchUserComplete]: (state, { payload: { user, username } }) => ({
|
[types.fetchUserComplete]: (state, { payload: { user, username } }) => ({
|
||||||
...state,
|
...state,
|
||||||
user,
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: user
|
||||||
|
},
|
||||||
appUsername: username,
|
appUsername: username,
|
||||||
userFetchState: {
|
userFetchState: {
|
||||||
pending: false,
|
pending: false,
|
||||||
@ -135,7 +147,20 @@ export const reducer = handleActions(
|
|||||||
errored: true,
|
errored: true,
|
||||||
error: payload
|
error: payload
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
[settingsTypes.submitNewUsernameComplete]: (state, { payload }) =>
|
||||||
|
payload
|
||||||
|
? {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[state.appUsername]: {
|
||||||
|
...state.user[state.appUsername],
|
||||||
|
username: payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: state
|
||||||
},
|
},
|
||||||
initialState
|
initialState
|
||||||
);
|
);
|
||||||
|
@ -2,8 +2,10 @@ import { combineReducers } from 'redux';
|
|||||||
|
|
||||||
import { reducer as app } from './';
|
import { reducer as app } from './';
|
||||||
import { reducer as flash } from '../components/Flash/redux';
|
import { reducer as flash } from '../components/Flash/redux';
|
||||||
|
import { reducer as settings } from './settings';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
app,
|
app,
|
||||||
flash
|
flash,
|
||||||
|
settings
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { all } from 'redux-saga/effects';
|
import { all } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { sagas as appSagas } from './';
|
import { sagas as appSagas } from './';
|
||||||
|
import { sagas as settingsSagas } from './settings';
|
||||||
|
|
||||||
export default function* rootSaga() {
|
export default function* rootSaga() {
|
||||||
yield all([...appSagas]);
|
yield all([...appSagas, ...settingsSagas]);
|
||||||
}
|
}
|
||||||
|
73
client/src/redux/settings/index.js
Normal file
73
client/src/redux/settings/index.js
Normal file
@ -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
|
||||||
|
);
|
41
client/src/redux/settings/settings-sagas.js
Normal file
41
client/src/redux/settings/settings-sagas.js
Normal file
@ -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)
|
||||||
|
];
|
||||||
|
}
|
@ -28,6 +28,10 @@ export function getShowCert(username, cert) {
|
|||||||
return get(`/certificate/showCert/${username}/${cert}`);
|
return get(`/certificate/showCert/${username}/${cert}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUsernameExists(username) {
|
||||||
|
return get(`/api/users/exists?username=${username}`);
|
||||||
|
}
|
||||||
|
|
||||||
/** POST **/
|
/** POST **/
|
||||||
|
|
||||||
export function postReportUser(body) {
|
export function postReportUser(body) {
|
||||||
@ -36,6 +40,10 @@ export function postReportUser(body) {
|
|||||||
|
|
||||||
/** PUT **/
|
/** PUT **/
|
||||||
|
|
||||||
|
export function putUpdateMyUsername(username) {
|
||||||
|
return put('/update-my-username', { username });
|
||||||
|
}
|
||||||
|
|
||||||
export function putUserAcceptsTerms(quincyEmails) {
|
export function putUserAcceptsTerms(quincyEmails) {
|
||||||
return put('/update-privacy-terms', { quincyEmails });
|
return put('/update-privacy-terms', { quincyEmails });
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user