feat(privacy): Add privacy settings
This commit is contained in:
@ -232,6 +232,7 @@
|
||||
"isLocked": true,
|
||||
"showAbout": false,
|
||||
"showCerts": false,
|
||||
"showDonation": false,
|
||||
"showHeatMap": false,
|
||||
"showLocation": false,
|
||||
"showName": false,
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
<Spacer />
|
||||
{/* <PrivacySettings />
|
||||
<Privacy />
|
||||
<Spacer />
|
||||
<EmailSettings />
|
||||
{/* <EmailSettings />
|
||||
<Spacer />
|
||||
<InternetSettings />
|
||||
<Spacer />
|
||||
|
221
client/src/components/settings/Privacy.js
Normal file
221
client/src/components/settings/Privacy.js
Normal 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 "about me"'
|
||||
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);
|
26
client/src/components/settings/SectionHeader.js
Normal file
26
client/src/components/settings/SectionHeader.js
Normal 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;
|
56
client/src/components/settings/ToggleSetting.js
Normal file
56
client/src/components/settings/ToggleSetting.js
Normal 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;
|
9
client/src/components/settings/toggle-setting.css
Normal file
9
client/src/components/settings/toggle-setting.css
Normal 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%;
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
];
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user