From 3a98e3cfa34b56baf8b450f2efa839882ae12a34 Mon Sep 17 00:00:00 2001 From: Bouncey Date: Tue, 18 Sep 2018 09:36:20 +0100 Subject: [PATCH] feat(privacy): Add privacy settings --- api-server/common/models/user.json | 1 + api-server/server/boot/settings.js | 39 ++-- client/src/client-only-routes/ShowSettings.js | 5 +- client/src/components/settings/Privacy.js | 221 ++++++++++++++++++ .../src/components/settings/SectionHeader.js | 26 +++ .../src/components/settings/ToggleSetting.js | 56 +++++ .../components/settings/toggle-setting.css | 9 + client/src/redux/settings/index.js | 10 +- client/src/redux/settings/settings-sagas.js | 18 +- client/src/utils/ajax.js | 4 + 10 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 client/src/components/settings/Privacy.js create mode 100644 client/src/components/settings/SectionHeader.js create mode 100644 client/src/components/settings/ToggleSetting.js create mode 100644 client/src/components/settings/toggle-setting.css diff --git a/api-server/common/models/user.json b/api-server/common/models/user.json index 2cc6a89b12..fc5d1aaec4 100644 --- a/api-server/common/models/user.json +++ b/api-server/common/models/user.json @@ -232,6 +232,7 @@ "isLocked": true, "showAbout": false, "showCerts": false, + "showDonation": false, "showHeatMap": false, "showLocation": false, "showName": false, diff --git a/api-server/server/boot/settings.js b/api-server/server/boot/settings.js index 87e6e9c4ca..e3e0e7d17e 100644 --- a/api-server/server/boot/settings.js +++ b/api-server/server/boot/settings.js @@ -34,7 +34,6 @@ export default function settingsController(app) { updateMyCurrentChallenge ); 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-theme', @@ -43,6 +42,7 @@ export default function settingsController(app) { createValidatorErrorHandler(alertTypes.danger), updateMyTheme ); + api.put('/update-my-about', ifNoUser401, updateMyAbout); api.put( '/update-my-email', ifNoUser401, @@ -50,7 +50,7 @@ export default function settingsController(app) { createValidatorErrorHandler(alertTypes.danger), 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-user-flag', ifNoUser401, updateUserFlag); @@ -69,6 +69,14 @@ const standardSuccessMessage = { 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) { const { user } = req; return user @@ -148,9 +156,11 @@ function updateMyProfileUI(req, res, next) { user, body: { profileUI } } = req; - return user - .updateMyProfileUI(profileUI) - .subscribe(message => res.json({ message }), next); + user.updateAttribute( + 'profileUI', + profileUI, + createStandardHandler(req, res, next) + ); } function updateMyProjects(req, res, next) { @@ -169,13 +179,10 @@ function updateMyAbout(req, res, next) { body: { name, location, about, picture } } = req; log(name, location, picture, about); - return user.updateAttributes({ name, location, about, picture }, err => { - if (err) { - res.status(500).json(standardErrorMessage); - return next(err); - } - return res.status(200).json(standardSuccessMessage); - }); + return user.updateAttributes( + { name, location, about, picture }, + createStandardHandler(req, res, next) + ); } function createUpdateMyUsername(app) { @@ -238,11 +245,5 @@ const updatePrivacyTerms = (req, res, next) => { function updateUserFlag(req, res, next) { const { user, body: update } = req; - user.updateAttributes(update, err => { - if (err) { - res.status(500).json(standardErrorMessage); - return next(err); - } - return res.status(200).json(standardSuccessMessage); - }); + user.updateAttributes(update, createStandardHandler(req, res, next)); } diff --git a/client/src/client-only-routes/ShowSettings.js b/client/src/client-only-routes/ShowSettings.js index ec352ac071..9eeb46571f 100644 --- a/client/src/client-only-routes/ShowSettings.js +++ b/client/src/client-only-routes/ShowSettings.js @@ -14,6 +14,7 @@ import Spacer from '../components/helpers/Spacer'; import Loader from '../components/helpers/Loader'; import { FullWidthRow } from '../components/helpers'; import About from '../components/settings/About'; +import Privacy from '../components/settings/Privacy'; const propTypes = { about: PropTypes.string, @@ -117,9 +118,9 @@ function ShowSettings(props) { username={username} /> - {/* + - + {/* diff --git a/client/src/components/settings/Privacy.js b/client/src/components/settings/Privacy.js new file mode 100644 index 0000000000..ce23e94d17 --- /dev/null +++ b/client/src/components/settings/Privacy.js @@ -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 ( +
+ Privacy Settings + +

+ The settings in this section enable you to control what is shown on + your freeCodeCamp public portfolio. +

+
+ + + + + + + + + + + +
+ + +

+ To see what data we hold on your account, click the 'Download your + data' button below +

+ +
+
+ ); + } +} + +PrivacySettings.displayName = 'PrivacySettings'; +PrivacySettings.propTypes = propTypes; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PrivacySettings); diff --git a/client/src/components/settings/SectionHeader.js b/client/src/components/settings/SectionHeader.js new file mode 100644 index 0000000000..e34f728bd0 --- /dev/null +++ b/client/src/components/settings/SectionHeader.js @@ -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 ( + +

{children}

+
+
+ ); +} + +SectionHeader.displayName = 'SectionHeader'; +SectionHeader.propTypes = propTypes; + +export default SectionHeader; diff --git a/client/src/components/settings/ToggleSetting.js b/client/src/components/settings/ToggleSetting.js new file mode 100644 index 0000000000..5853a74042 --- /dev/null +++ b/client/src/components/settings/ToggleSetting.js @@ -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 ( + +
+ + + {action} + {explain ? ( + + {explain} + + ) : null} + + + +
+ +
+ ); +} + +ToggleSetting.displayName = 'ToggleSetting'; +ToggleSetting.propTypes = propTypes; diff --git a/client/src/components/settings/toggle-setting.css b/client/src/components/settings/toggle-setting.css new file mode 100644 index 0000000000..7b3616d219 --- /dev/null +++ b/client/src/components/settings/toggle-setting.css @@ -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%; +} \ No newline at end of file diff --git a/client/src/redux/settings/index.js b/client/src/redux/settings/index.js index 41f2dd3706..dacdd1bd57 100644 --- a/client/src/redux/settings/index.js +++ b/client/src/redux/settings/index.js @@ -24,7 +24,8 @@ export const types = createTypes( ...createAsyncTypes('validateUsername'), ...createAsyncTypes('submitNewAbout'), ...createAsyncTypes('submitNewUsername'), - ...createAsyncTypes('updateUserFlag') + ...createAsyncTypes('updateUserFlag'), + ...createAsyncTypes('submitProfileUI') ], ns ); @@ -49,6 +50,13 @@ export const submitNewUsernameError = createAction( 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 updateUserFlagComplete = createAction( types.updateUserFlagComplete, diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 3cf07c98e3..0a5b0b5dfc 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -9,11 +9,14 @@ import { submitNewAboutComplete, submitNewAboutError, submitNewUsernameComplete, - submitNewUsernameError + submitNewUsernameError, + submitProfileUIComplete, + submitProfileUIError } from './'; import { getUsernameExists, putUpdateMyAbout, + putUpdateMyProfileUI, putUpdateMyUsername, putUpdateUserFlag } 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 }) { try { const { data: response } = yield call(putUpdateUserFlag, update); @@ -66,6 +79,7 @@ export function createSettingsSagas(types) { takeEvery(types.updateUserFlag, updateUserFlagSaga), takeLatest(types.submitNewAbout, submitNewAboutSaga), takeLatest(types.submitNewUsername, submitNewUsernameSaga), - takeLatest(types.validateUsername, validateUsernameSaga) + takeLatest(types.validateUsername, validateUsernameSaga), + takeLatest(types.submitProfileUI, sumbitProfileUISaga) ]; } diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index 841c7b41d1..0d7b1a010c 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -48,6 +48,10 @@ export function putUpdateMyUsername(username) { return put('/update-my-username', { username }); } +export function putUpdateMyProfileUI(profileUI) { + return put('/update-my-profileui', { profileUI }); +} + export function putUpdateUserFlag(update) { return put('/update-user-flag', update); }