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);
}