feat(privacy): Add privacy settings

This commit is contained in:
Bouncey
2018-09-18 09:36:20 +01:00
committed by Stuart Taylor
parent 83b74d14f3
commit 3a98e3cfa3
10 changed files with 365 additions and 24 deletions

View File

@ -232,6 +232,7 @@
"isLocked": true, "isLocked": true,
"showAbout": false, "showAbout": false,
"showCerts": false, "showCerts": false,
"showDonation": false,
"showHeatMap": false, "showHeatMap": false,
"showLocation": false, "showLocation": false,
"showName": false, "showName": false,

View File

@ -34,7 +34,6 @@ export default function settingsController(app) {
updateMyCurrentChallenge updateMyCurrentChallenge
); );
api.post('/update-my-portfolio', ifNoUser401, updateMyPortfolio); api.post('/update-my-portfolio', ifNoUser401, updateMyPortfolio);
api.post('/update-my-profile-ui', ifNoUser401, updateMyProfileUI);
api.post('/update-my-projects', ifNoUser401, updateMyProjects); api.post('/update-my-projects', ifNoUser401, updateMyProjects);
api.post( api.post(
'/update-my-theme', '/update-my-theme',
@ -43,6 +42,7 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger), createValidatorErrorHandler(alertTypes.danger),
updateMyTheme updateMyTheme
); );
api.put('/update-my-about', ifNoUser401, updateMyAbout);
api.put( api.put(
'/update-my-email', '/update-my-email',
ifNoUser401, ifNoUser401,
@ -50,7 +50,7 @@ export default function settingsController(app) {
createValidatorErrorHandler(alertTypes.danger), createValidatorErrorHandler(alertTypes.danger),
updateMyEmail updateMyEmail
); );
api.put('/update-my-about', ifNoUser401, updateMyAbout); api.put('/update-my-profileui', ifNoUser401, updateMyProfileUI);
api.put('/update-my-username', ifNoUser401, updateMyUsername); api.put('/update-my-username', ifNoUser401, updateMyUsername);
api.put('/update-user-flag', ifNoUser401, updateUserFlag); api.put('/update-user-flag', ifNoUser401, updateUserFlag);
@ -69,6 +69,14 @@ const standardSuccessMessage = {
message: 'We have updated your preferences' message: 'We have updated your preferences'
}; };
const createStandardHandler = (req, res, next) => err => {
if (err) {
res.status(500).json(standardErrorMessage);
return next(err);
}
return res.status(200).json(standardSuccessMessage);
};
function refetchCompletedChallenges(req, res, next) { function refetchCompletedChallenges(req, res, next) {
const { user } = req; const { user } = req;
return user return user
@ -148,9 +156,11 @@ function updateMyProfileUI(req, res, next) {
user, user,
body: { profileUI } body: { profileUI }
} = req; } = req;
return user user.updateAttribute(
.updateMyProfileUI(profileUI) 'profileUI',
.subscribe(message => res.json({ message }), next); profileUI,
createStandardHandler(req, res, next)
);
} }
function updateMyProjects(req, res, next) { function updateMyProjects(req, res, next) {
@ -169,13 +179,10 @@ function updateMyAbout(req, res, next) {
body: { name, location, about, picture } body: { name, location, about, picture }
} = req; } = req;
log(name, location, picture, about); log(name, location, picture, about);
return user.updateAttributes({ name, location, about, picture }, err => { return user.updateAttributes(
if (err) { { name, location, about, picture },
res.status(500).json(standardErrorMessage); createStandardHandler(req, res, next)
return next(err); );
}
return res.status(200).json(standardSuccessMessage);
});
} }
function createUpdateMyUsername(app) { function createUpdateMyUsername(app) {
@ -238,11 +245,5 @@ const updatePrivacyTerms = (req, res, next) => {
function updateUserFlag(req, res, next) { function updateUserFlag(req, res, next) {
const { user, body: update } = req; const { user, body: update } = req;
user.updateAttributes(update, err => { user.updateAttributes(update, createStandardHandler(req, res, next));
if (err) {
res.status(500).json(standardErrorMessage);
return next(err);
}
return res.status(200).json(standardSuccessMessage);
});
} }

View File

@ -14,6 +14,7 @@ import Spacer from '../components/helpers/Spacer';
import Loader from '../components/helpers/Loader'; import Loader from '../components/helpers/Loader';
import { FullWidthRow } from '../components/helpers'; import { FullWidthRow } from '../components/helpers';
import About from '../components/settings/About'; import About from '../components/settings/About';
import Privacy from '../components/settings/Privacy';
const propTypes = { const propTypes = {
about: PropTypes.string, about: PropTypes.string,
@ -117,9 +118,9 @@ function ShowSettings(props) {
username={username} username={username}
/> />
<Spacer /> <Spacer />
{/* <PrivacySettings /> <Privacy />
<Spacer /> <Spacer />
<EmailSettings /> {/* <EmailSettings />
<Spacer /> <Spacer />
<InternetSettings /> <InternetSettings />
<Spacer /> <Spacer />

View File

@ -0,0 +1,221 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Form } from '@freecodecamp/react-bootstrap';
import { isEqual } from 'lodash';
import { userSelector } from '../../redux';
import { submitProfileUI } from '../../redux/settings';
import FullWidthRow from '../helpers/FullWidthRow';
import Spacer from '../helpers/Spacer';
import ToggleSetting from './ToggleSetting';
import SectionHeader from './SectionHeader';
const mapStateToProps = createSelector(userSelector, user => ({
...user.profileUI,
user
}));
const mapDispatchToProps = dispatch =>
bindActionCreators({ submitProfileUI }, dispatch);
const propTypes = {
isLocked: PropTypes.bool,
showAbout: PropTypes.bool,
showCerts: PropTypes.bool,
showDonation: PropTypes.bool,
showHeatMap: PropTypes.bool,
showLocation: PropTypes.bool,
showName: PropTypes.bool,
showPoints: PropTypes.bool,
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool,
submitProfileUI: PropTypes.func.isRequired,
user: PropTypes.object
};
class PrivacySettings extends Component {
constructor(props) {
super(props);
const originalProfileUI = { ...props.user.profileUI };
this.state = {
privacyValues: {
...originalProfileUI
},
originalProfileUI: { ...originalProfileUI }
};
}
componentDidUpdate() {
const { profileUI: currentPropsProfileUI } = this.props.user;
const { originalProfileUI } = this.state;
if (!isEqual(originalProfileUI, currentPropsProfileUI)) {
/* eslint-disable-next-line react/no-did-update-set-state */
return this.setState(state => ({
...state,
originalProfileUI: { ...currentPropsProfileUI },
privacyValues: { ...currentPropsProfileUI }
}));
}
return null;
}
handleSubmit = e => e.preventDefault();
toggleFlag = flag => () =>
this.setState(
state => ({
privacyValues: {
...state.privacyValues,
[flag]: !state.privacyValues[flag]
}
}),
() => this.props.submitProfileUI(this.state.privacyValues)
);
render() {
const {
privacyValues: {
isLocked = true,
showAbout = false,
showCerts = false,
showDonation = false,
showHeatMap = false,
showLocation = false,
showName = false,
showPoints = false,
showPortfolio = false,
showTimeLine = false
}
} = this.state;
const { user } = this.props;
return (
<div className='privacy-settings'>
<SectionHeader>Privacy Settings</SectionHeader>
<FullWidthRow>
<p>
The settings in this section enable you to control what is shown on
your freeCodeCamp public portfolio.
</p>
<Form inline={true} onSubmit={this.handleSubmit}>
<ToggleSetting
action='My profile'
explain='Your certifications will be disabled'
flag={isLocked}
flagName='isLocked'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('isLocked')}
/>
<ToggleSetting
action='My name'
flag={!showName}
flagName='name'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showName')}
/>
<ToggleSetting
action='My location'
flag={!showLocation}
flagName='showLocation'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showLocation')}
/>
<ToggleSetting
action='My &quot;about me&quot;'
flag={!showAbout}
flagName='showAbout'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showAbout')}
/>
<ToggleSetting
action='My points'
flag={!showPoints}
flagName='showPoints'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showPoints')}
/>
<ToggleSetting
action='My heat map'
flag={!showHeatMap}
flagName='showHeatMap'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showHeatMap')}
/>
<ToggleSetting
action='My certifications'
explain='Your certifications will be disabled'
flag={!showCerts}
flagName='showCerts'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showCerts')}
/>
<ToggleSetting
action='My portfolio'
flag={!showPortfolio}
flagName='showPortfolio'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showPortfolio')}
/>
<ToggleSetting
action='My time line'
explain='Your certifications will be disabled'
flag={!showTimeLine}
flagName='showTimeLine'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showTimeLine')}
/>
<ToggleSetting
action='My donations'
flag={!showDonation}
flagName='showPortfolio'
offLabel='Public'
onLabel='Private'
toggleFlag={this.toggleFlag('showDonation')}
/>
</Form>
</FullWidthRow>
<FullWidthRow>
<Spacer />
<p>
To see what data we hold on your account, click the 'Download your
data' button below
</p>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
download={`${user.username}.json`}
href={`data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(user)
)}`}
>
Download your data
</Button>
</FullWidthRow>
</div>
);
}
}
PrivacySettings.displayName = 'PrivacySettings';
PrivacySettings.propTypes = propTypes;
export default connect(
mapStateToProps,
mapDispatchToProps
)(PrivacySettings);

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import FullWidthRow from '../helpers/FullWidthRow';
const propTypes = {
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
PropTypes.node
])
};
function SectionHeader({ children }) {
return (
<FullWidthRow>
<h2 className='text-center'>{children}</h2>
<hr />
</FullWidthRow>
);
}
SectionHeader.displayName = 'SectionHeader';
SectionHeader.propTypes = propTypes;
export default SectionHeader;

View File

@ -0,0 +1,56 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
FormGroup,
ControlLabel,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import TB from '../helpers/ToggleButton';
import { ButtonSpacer } from '../helpers';
import './toggle-setting.css';
const propTypes = {
action: PropTypes.string.isRequired,
explain: PropTypes.string,
flag: PropTypes.bool.isRequired,
flagName: PropTypes.string.isRequired,
toggleFlag: PropTypes.func.isRequired
};
export default function ToggleSetting({
action,
explain,
flag,
flagName,
toggleFlag,
...restProps
}) {
return (
<Fragment>
<div className='toggle-setting-container'>
<FormGroup>
<ControlLabel className='toggle-label' htmlFor={flagName}>
<strong>{action}</strong>
{explain ? (
<HelpBlock>
<em>{explain}</em>
</HelpBlock>
) : null}
</ControlLabel>
<TB
name={flagName}
onChange={toggleFlag}
value={flag}
{...restProps}
/>
</FormGroup>
</div>
<ButtonSpacer />
</Fragment>
);
}
ToggleSetting.displayName = 'ToggleSetting';
ToggleSetting.propTypes = propTypes;

View File

@ -0,0 +1,9 @@
.toggle-setting-container .form-group {
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle-setting-container .form-group label {
max-width: 50%;
}

View File

@ -24,7 +24,8 @@ export const types = createTypes(
...createAsyncTypes('validateUsername'), ...createAsyncTypes('validateUsername'),
...createAsyncTypes('submitNewAbout'), ...createAsyncTypes('submitNewAbout'),
...createAsyncTypes('submitNewUsername'), ...createAsyncTypes('submitNewUsername'),
...createAsyncTypes('updateUserFlag') ...createAsyncTypes('updateUserFlag'),
...createAsyncTypes('submitProfileUI')
], ],
ns ns
); );
@ -49,6 +50,13 @@ export const submitNewUsernameError = createAction(
types.submitNewUsernameError types.submitNewUsernameError
); );
export const submitProfileUI = createAction(types.submitProfileUI);
export const submitProfileUIComplete = createAction(
types.submitProfileUIComplete,
checkForSuccessPayload
);
export const submitProfileUIError = createAction(types.submitProfileUIError);
export const updateUserFlag = createAction(types.updateUserFlag); export const updateUserFlag = createAction(types.updateUserFlag);
export const updateUserFlagComplete = createAction( export const updateUserFlagComplete = createAction(
types.updateUserFlagComplete, types.updateUserFlagComplete,

View File

@ -9,11 +9,14 @@ import {
submitNewAboutComplete, submitNewAboutComplete,
submitNewAboutError, submitNewAboutError,
submitNewUsernameComplete, submitNewUsernameComplete,
submitNewUsernameError submitNewUsernameError,
submitProfileUIComplete,
submitProfileUIError
} from './'; } from './';
import { import {
getUsernameExists, getUsernameExists,
putUpdateMyAbout, putUpdateMyAbout,
putUpdateMyProfileUI,
putUpdateMyUsername, putUpdateMyUsername,
putUpdateUserFlag putUpdateUserFlag
} from '../../utils/ajax'; } from '../../utils/ajax';
@ -39,6 +42,16 @@ function* submitNewUsernameSaga({ payload: username }) {
} }
} }
function* sumbitProfileUISaga({ payload }) {
try {
const { data: response } = yield call(putUpdateMyProfileUI, payload);
yield put(submitProfileUIComplete({ ...response, payload }));
yield put(createFlashMessage(response));
} catch (e) {
yield put(submitProfileUIError);
}
}
function* updateUserFlagSaga({ payload: update }) { function* updateUserFlagSaga({ payload: update }) {
try { try {
const { data: response } = yield call(putUpdateUserFlag, update); const { data: response } = yield call(putUpdateUserFlag, update);
@ -66,6 +79,7 @@ export function createSettingsSagas(types) {
takeEvery(types.updateUserFlag, updateUserFlagSaga), takeEvery(types.updateUserFlag, updateUserFlagSaga),
takeLatest(types.submitNewAbout, submitNewAboutSaga), takeLatest(types.submitNewAbout, submitNewAboutSaga),
takeLatest(types.submitNewUsername, submitNewUsernameSaga), takeLatest(types.submitNewUsername, submitNewUsernameSaga),
takeLatest(types.validateUsername, validateUsernameSaga) takeLatest(types.validateUsername, validateUsernameSaga),
takeLatest(types.submitProfileUI, sumbitProfileUISaga)
]; ];
} }

View File

@ -48,6 +48,10 @@ export function putUpdateMyUsername(username) {
return put('/update-my-username', { username }); return put('/update-my-username', { username });
} }
export function putUpdateMyProfileUI(profileUI) {
return put('/update-my-profileui', { profileUI });
}
export function putUpdateUserFlag(update) { export function putUpdateUserFlag(update) {
return put('/update-user-flag', update); return put('/update-user-flag', update);
} }