Feat(privacy): Add granular privacy controls of public profile (#17178)
* feat(privacy): Add granular privacy controls of public profile * feat(certs): Hide certs if showCerts is false
This commit is contained in:
committed by
Quincy Larson
parent
a1f2fc7c5c
commit
bb4bcbfb45
@ -20,6 +20,7 @@ export const types = createTypes([
|
|||||||
'optoUpdatePortfolio',
|
'optoUpdatePortfolio',
|
||||||
'regresPortfolio',
|
'regresPortfolio',
|
||||||
'resetFullBlocks',
|
'resetFullBlocks',
|
||||||
|
'updateLocalProfileUI',
|
||||||
'updateMultipleUserFlags',
|
'updateMultipleUserFlags',
|
||||||
'updateTheme',
|
'updateTheme',
|
||||||
'updateUserFlag',
|
'updateUserFlag',
|
||||||
@ -56,6 +57,8 @@ export const updateUserLang = createAction(
|
|||||||
(username, lang) => ({ username, languageTag: lang })
|
(username, lang) => ({ username, languageTag: lang })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateLocalProfileUI = createAction(types.updateLocalProfileUI);
|
||||||
|
|
||||||
export const resetFullBlocks = createAction(types.resetFullBlocks);
|
export const resetFullBlocks = createAction(types.resetFullBlocks);
|
||||||
|
|
||||||
export const updateUserCurrentChallenge = createAction(
|
export const updateUserCurrentChallenge = createAction(
|
||||||
@ -294,6 +297,23 @@ export default composeReducers(
|
|||||||
languageTag
|
languageTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
[types.updateLocalProfileUI]:
|
||||||
|
(
|
||||||
|
state,
|
||||||
|
{ payload: { username, profileUI } }
|
||||||
|
) => ({
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[username]: {
|
||||||
|
...state.user[username],
|
||||||
|
profileUI: {
|
||||||
|
...state.user[username].profileUI,
|
||||||
|
...profileUI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
defaultState
|
defaultState
|
||||||
|
@ -38,7 +38,20 @@ const mapStateToProps = createSelector(
|
|||||||
userFoundSelector,
|
userFoundSelector,
|
||||||
(
|
(
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
{ isLocked, username: requestedUsername },
|
{
|
||||||
|
username: requestedUsername,
|
||||||
|
profileUI: {
|
||||||
|
isLocked,
|
||||||
|
showAbout,
|
||||||
|
showCerts,
|
||||||
|
showHeatMap,
|
||||||
|
showLocation,
|
||||||
|
showName,
|
||||||
|
showPoints,
|
||||||
|
showPortfolio,
|
||||||
|
showTimeLine
|
||||||
|
} = {}
|
||||||
|
},
|
||||||
{ username: paramsUsername },
|
{ username: paramsUsername },
|
||||||
currentUsername,
|
currentUsername,
|
||||||
showLoading,
|
showLoading,
|
||||||
@ -47,12 +60,20 @@ const mapStateToProps = createSelector(
|
|||||||
isSignedIn,
|
isSignedIn,
|
||||||
currentUsername,
|
currentUsername,
|
||||||
isCurrentUserProfile: paramsUsername === currentUsername,
|
isCurrentUserProfile: paramsUsername === currentUsername,
|
||||||
isLocked,
|
|
||||||
isUserFound,
|
isUserFound,
|
||||||
fetchOtherUserCompleted: typeof isUserFound === 'boolean',
|
fetchOtherUserCompleted: typeof isUserFound === 'boolean',
|
||||||
paramsUsername,
|
paramsUsername,
|
||||||
requestedUsername,
|
requestedUsername,
|
||||||
showLoading
|
isLocked,
|
||||||
|
showLoading,
|
||||||
|
showAbout,
|
||||||
|
showCerts,
|
||||||
|
showHeatMap,
|
||||||
|
showLocation,
|
||||||
|
showName,
|
||||||
|
showPoints,
|
||||||
|
showPortfolio,
|
||||||
|
showTimeLine
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -71,7 +92,15 @@ const propTypes = {
|
|||||||
isUserFound: PropTypes.bool,
|
isUserFound: PropTypes.bool,
|
||||||
paramsUsername: PropTypes.string,
|
paramsUsername: PropTypes.string,
|
||||||
requestedUsername: PropTypes.string,
|
requestedUsername: PropTypes.string,
|
||||||
|
showAbout: PropTypes.bool,
|
||||||
|
showCerts: PropTypes.bool,
|
||||||
|
showHeatMap: PropTypes.bool,
|
||||||
showLoading: PropTypes.bool,
|
showLoading: PropTypes.bool,
|
||||||
|
showLocation: PropTypes.bool,
|
||||||
|
showName: PropTypes.bool,
|
||||||
|
showPoints: PropTypes.bool,
|
||||||
|
showPortfolio: PropTypes.bool,
|
||||||
|
showTimeLine: PropTypes.bool,
|
||||||
updateTitle: PropTypes.func.isRequired
|
updateTitle: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,7 +122,15 @@ class Profile extends Component {
|
|||||||
isLocked,
|
isLocked,
|
||||||
isUserFound,
|
isUserFound,
|
||||||
isCurrentUserProfile,
|
isCurrentUserProfile,
|
||||||
paramsUsername
|
paramsUsername,
|
||||||
|
showAbout,
|
||||||
|
showLocation,
|
||||||
|
showName,
|
||||||
|
showPoints,
|
||||||
|
showHeatMap,
|
||||||
|
showCerts,
|
||||||
|
showPortfolio,
|
||||||
|
showTimeLine
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const takeMeToChallenges = (
|
const takeMeToChallenges = (
|
||||||
<a href='/challenges/current-challenge'>
|
<a href='/challenges/current-challenge'>
|
||||||
@ -113,8 +150,8 @@ class Profile extends Component {
|
|||||||
<Alert bsStyle='info'>
|
<Alert bsStyle='info'>
|
||||||
<p>
|
<p>
|
||||||
{
|
{
|
||||||
'In order to view their progress through the freeCodeCamp ' +
|
'In order to view their freeCodeCamp certiciations, ' +
|
||||||
'curriculum, they need to make all of thie solutions public'
|
'they need to make their profile public'
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
@ -136,11 +173,16 @@ class Profile extends Component {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CamperHOC />
|
<CamperHOC
|
||||||
<HeatMap />
|
showAbout={ showAbout }
|
||||||
<Certificates />
|
showLocation={ showLocation }
|
||||||
<Portfolio />
|
showName={ showName }
|
||||||
<Timeline className='timelime-container' />
|
showPoints={ showPoints }
|
||||||
|
/>
|
||||||
|
{ showHeatMap ? <HeatMap /> : null }
|
||||||
|
{ showCerts ? <Certificates /> : null }
|
||||||
|
{ showPortfolio ? <Portfolio /> : null }
|
||||||
|
{ showTimeLine ? <Timeline className='timelime-container' /> : null }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,10 @@ const propTypes = {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
|
showAbout: PropTypes.bool,
|
||||||
|
showLocation: PropTypes.bool,
|
||||||
|
showName: PropTypes.bool,
|
||||||
|
showPoints: PropTypes.bool,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,17 +44,21 @@ function CamperHOC({
|
|||||||
location,
|
location,
|
||||||
points,
|
points,
|
||||||
picture,
|
picture,
|
||||||
about
|
about,
|
||||||
|
showAbout,
|
||||||
|
showLocation,
|
||||||
|
showName,
|
||||||
|
showPoints
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Camper
|
<Camper
|
||||||
about={ about }
|
about={ showAbout && about }
|
||||||
location={ location }
|
location={ showLocation && location }
|
||||||
name={ name }
|
name={ showName && name }
|
||||||
picture={ picture }
|
picture={ picture }
|
||||||
points={ points }
|
points={ showPoints && points }
|
||||||
username={ username }
|
username={ username }
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
|
@ -15,6 +15,7 @@ import EmailSettings from './components/Email-Settings.jsx';
|
|||||||
import DangerZone from './components/DangerZone.jsx';
|
import DangerZone from './components/DangerZone.jsx';
|
||||||
import CertificationSettings from './components/Cert-Settings.jsx';
|
import CertificationSettings from './components/Cert-Settings.jsx';
|
||||||
import PortfolioSettings from './components/Portfolio-Settings.jsx';
|
import PortfolioSettings from './components/Portfolio-Settings.jsx';
|
||||||
|
import PrivacySettings from './components/Privacy-Settings.jsx';
|
||||||
import Honesty from './components/Honesty.jsx';
|
import Honesty from './components/Honesty.jsx';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -101,6 +102,8 @@ export class Settings extends React.Component {
|
|||||||
<h1 className='text-center'>{ `Account Settings for ${username}` }</h1>
|
<h1 className='text-center'>{ `Account Settings for ${username}` }</h1>
|
||||||
<AboutSettings />
|
<AboutSettings />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<PrivacySettings />
|
||||||
|
<Spacer />
|
||||||
<EmailSettings />
|
<EmailSettings />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<InternetSettings />
|
<InternetSettings />
|
||||||
|
@ -9,15 +9,12 @@ import {
|
|||||||
} from 'react-bootstrap';
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
import { FullWidthRow, Spacer } from '../../../helperComponents';
|
import { FullWidthRow, Spacer } from '../../../helperComponents';
|
||||||
import LockedSettings from './Locked-Settings.jsx';
|
|
||||||
import ThemeSettings from './ThemeSettings.jsx';
|
import ThemeSettings from './ThemeSettings.jsx';
|
||||||
import Camper from './Camper.jsx';
|
import Camper from './Camper.jsx';
|
||||||
import UsernameSettings from './UsernameSettings.jsx';
|
import UsernameSettings from './UsernameSettings.jsx';
|
||||||
import SectionHeader from './SectionHeader.jsx';
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
import { userSelector, toggleNightMode } from '../../../redux';
|
import { userSelector, toggleNightMode } from '../../../redux';
|
||||||
import {
|
import { updateUserBackend } from '../redux';
|
||||||
updateUserBackend
|
|
||||||
} from '../redux';
|
|
||||||
import {
|
import {
|
||||||
BlockSaveButton,
|
BlockSaveButton,
|
||||||
BlockSaveWrapper,
|
BlockSaveWrapper,
|
||||||
@ -33,7 +30,6 @@ const mapStateToProps = createSelector(
|
|||||||
(
|
(
|
||||||
{
|
{
|
||||||
about,
|
about,
|
||||||
isLocked,
|
|
||||||
location,
|
location,
|
||||||
name,
|
name,
|
||||||
picture,
|
picture,
|
||||||
@ -45,7 +41,6 @@ const mapStateToProps = createSelector(
|
|||||||
about,
|
about,
|
||||||
currentTheme: theme,
|
currentTheme: theme,
|
||||||
initialValues: { name, location, about, picture },
|
initialValues: { name, location, about, picture },
|
||||||
isLocked,
|
|
||||||
location,
|
location,
|
||||||
name,
|
name,
|
||||||
picture,
|
picture,
|
||||||
@ -79,7 +74,6 @@ const propTypes = {
|
|||||||
currentTheme: PropTypes.string,
|
currentTheme: PropTypes.string,
|
||||||
fields: PropTypes.object,
|
fields: PropTypes.object,
|
||||||
handleSubmit: PropTypes.func.isRequired,
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
isLocked: PropTypes.bool,
|
|
||||||
location: PropTypes.string,
|
location: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
@ -163,14 +157,11 @@ class AboutSettings extends PureComponent {
|
|||||||
currentTheme,
|
currentTheme,
|
||||||
fields: { _meta: { allPristine } },
|
fields: { _meta: { allPristine } },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
isLocked,
|
|
||||||
toggleNightMode,
|
toggleNightMode,
|
||||||
updateUserBackend,
|
|
||||||
username
|
username
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { view } = this.state;
|
const { view } = this.state;
|
||||||
|
|
||||||
const toggleIsLocked = () => updateUserBackend({ isLocked: !isLocked });
|
|
||||||
const toggleTheme = () => toggleNightMode(username, currentTheme);
|
const toggleTheme = () => toggleNightMode(username, currentTheme);
|
||||||
return (
|
return (
|
||||||
<div className='about-settings'>
|
<div className='about-settings'>
|
||||||
@ -205,12 +196,6 @@ class AboutSettings extends PureComponent {
|
|||||||
</form>
|
</form>
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<FullWidthRow>
|
|
||||||
<LockedSettings
|
|
||||||
isLocked={ isLocked }
|
|
||||||
toggleIsLocked={ toggleIsLocked }
|
|
||||||
/>
|
|
||||||
</FullWidthRow>
|
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<ThemeSettings
|
<ThemeSettings
|
||||||
currentTheme={ currentTheme }
|
currentTheme={ currentTheme }
|
||||||
|
@ -42,9 +42,13 @@ function Camper({
|
|||||||
{ name && <p className='text-center name'>{ name }</p> }
|
{ name && <p className='text-center name'>{ name }</p> }
|
||||||
{ location && <p className='text-center location'>{ location }</p> }
|
{ location && <p className='text-center location'>{ location }</p> }
|
||||||
{ about && <p className='bio text-center'>{ about }</p> }
|
{ about && <p className='bio text-center'>{ about }</p> }
|
||||||
|
{
|
||||||
|
typeof points === 'number' ? (
|
||||||
<p className='text-center points'>
|
<p className='text-center points'>
|
||||||
{ `${points} ${pluralise('point', points > 1)}` }
|
{ `${points} ${pluralise('point', points > 1)}` }
|
||||||
</p>
|
</p>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
ControlLabel
|
|
||||||
} from 'react-bootstrap';
|
|
||||||
|
|
||||||
import TB from '../Toggle-Button';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
isLocked: PropTypes.bool,
|
|
||||||
toggleIsLocked: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LockSettings({ isLocked, toggleIsLocked }) {
|
|
||||||
return (
|
|
||||||
<Row className='inline-form'>
|
|
||||||
<Col sm={ 8 } xs={ 12 }>
|
|
||||||
<ControlLabel htmlFor='isLocked'>
|
|
||||||
<p>
|
|
||||||
<strong>
|
|
||||||
Make all of my solutions private
|
|
||||||
<br />
|
|
||||||
<em>(this disables your certificates)</em>
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
</ControlLabel>
|
|
||||||
</Col>
|
|
||||||
<Col sm={ 4 } xs={ 12 }>
|
|
||||||
<TB
|
|
||||||
name='isLocked'
|
|
||||||
onChange={ toggleIsLocked }
|
|
||||||
value={ isLocked }
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LockSettings.displayName = 'LockSettings';
|
|
||||||
LockSettings.propTypes = propTypes;
|
|
126
common/app/routes/Settings/components/Privacy-Settings.jsx
Normal file
126
common/app/routes/Settings/components/Privacy-Settings.jsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { updateMyProfileUI } from '../redux';
|
||||||
|
import { userSelector } from '../../../redux';
|
||||||
|
|
||||||
|
import { FullWidthRow } from '../../../helperComponents';
|
||||||
|
import SectionHeader from './SectionHeader.jsx';
|
||||||
|
import ToggleSetting from './ToggleSetting.jsx';
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
userSelector,
|
||||||
|
({
|
||||||
|
profileUI = {}
|
||||||
|
}) => ({
|
||||||
|
...profileUI
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch =>
|
||||||
|
bindActionCreators({ updateMyProfileUI }, dispatch);
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isLocked: PropTypes.bool,
|
||||||
|
showAbout: PropTypes.bool,
|
||||||
|
showCerts: PropTypes.bool,
|
||||||
|
showHeatMap: PropTypes.bool,
|
||||||
|
showLocation: PropTypes.bool,
|
||||||
|
showName: PropTypes.bool,
|
||||||
|
showPoints: PropTypes.bool,
|
||||||
|
showPortfolio: PropTypes.bool,
|
||||||
|
showTimeLine: PropTypes.bool,
|
||||||
|
updateMyProfileUI: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function PrivacySettings(props) {
|
||||||
|
const {
|
||||||
|
isLocked,
|
||||||
|
showAbout,
|
||||||
|
showCerts,
|
||||||
|
showHeatMap,
|
||||||
|
showLocation,
|
||||||
|
showName,
|
||||||
|
showPoints,
|
||||||
|
showPortfolio,
|
||||||
|
showTimeLine,
|
||||||
|
updateMyProfileUI
|
||||||
|
} = props;
|
||||||
|
const toggleFlag = flag =>
|
||||||
|
() => updateMyProfileUI({ profileUI: { [flag]: !props[flag] } });
|
||||||
|
return (
|
||||||
|
<div className='privacy-settings'>
|
||||||
|
<SectionHeader>Privacy Settings</SectionHeader>
|
||||||
|
<FullWidthRow>
|
||||||
|
<p>
|
||||||
|
The settings in this section enable you to control what is show on{' '}
|
||||||
|
your freeCodeCamp public profile.
|
||||||
|
</p>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my profile completely private'
|
||||||
|
explain='Your certifications will be disabled'
|
||||||
|
flag={ isLocked }
|
||||||
|
flagName='isLocked'
|
||||||
|
toggleFlag={ toggleFlag('isLocked') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my name completely private'
|
||||||
|
flag={ !showName }
|
||||||
|
flagName='name'
|
||||||
|
toggleFlag={ toggleFlag('showName') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my location completely private'
|
||||||
|
flag={ !showLocation }
|
||||||
|
flagName='showLocation'
|
||||||
|
toggleFlag={ toggleFlag('showLocation') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my "about me" completely private'
|
||||||
|
flag={ !showAbout }
|
||||||
|
flagName='showAbout'
|
||||||
|
toggleFlag={ toggleFlag('showAbout') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my points completely private'
|
||||||
|
flag={ !showPoints }
|
||||||
|
flagName='showPoints'
|
||||||
|
toggleFlag={ toggleFlag('showPoints') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my heat map completely private'
|
||||||
|
flag={ !showHeatMap }
|
||||||
|
flagName='showHeatMap'
|
||||||
|
toggleFlag={ toggleFlag('showHeatMap') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my certifications completely private'
|
||||||
|
explain='Your certifications will be disabled'
|
||||||
|
flag={ !showCerts }
|
||||||
|
flagName='showCerts'
|
||||||
|
toggleFlag={ toggleFlag('showCerts') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my portfolio completely private'
|
||||||
|
flag={ !showPortfolio }
|
||||||
|
flagName='showPortfolio'
|
||||||
|
toggleFlag={ toggleFlag('showPortfolio') }
|
||||||
|
/>
|
||||||
|
<ToggleSetting
|
||||||
|
action='Make my time line completely private'
|
||||||
|
flag={ !showTimeLine }
|
||||||
|
flagName='showTimeLine'
|
||||||
|
toggleFlag={ toggleFlag('showTimeLine') }
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivacySettings.displayName = 'PrivacySettings';
|
||||||
|
PrivacySettings.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PrivacySettings);
|
53
common/app/routes/Settings/components/ToggleSetting.jsx
Normal file
53
common/app/routes/Settings/components/ToggleSetting.jsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
ControlLabel
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import TB from '../Toggle-Button';
|
||||||
|
|
||||||
|
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
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Row className='inline-form'>
|
||||||
|
<Col sm={ 8 } xs={ 12 }>
|
||||||
|
<ControlLabel htmlFor={ flagName }>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
{ action }
|
||||||
|
</strong>
|
||||||
|
<br />
|
||||||
|
{
|
||||||
|
explain ? <em>{explain}</em> : null
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</ControlLabel>
|
||||||
|
</Col>
|
||||||
|
<Col sm={ 4 } xs={ 12 }>
|
||||||
|
<TB
|
||||||
|
name={ flagName }
|
||||||
|
onChange={ toggleFlag }
|
||||||
|
value={ flag }
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleSetting.displayName = 'ToggleSetting';
|
||||||
|
ToggleSetting.propTypes = propTypes;
|
@ -34,6 +34,7 @@ export const types = createTypes([
|
|||||||
createAsyncTypes('updateUserBackend'),
|
createAsyncTypes('updateUserBackend'),
|
||||||
createAsyncTypes('deletePortfolio'),
|
createAsyncTypes('deletePortfolio'),
|
||||||
createAsyncTypes('updateMyPortfolio'),
|
createAsyncTypes('updateMyPortfolio'),
|
||||||
|
createAsyncTypes('updateMyProfileUI'),
|
||||||
'updateNewUsernameValidity',
|
'updateNewUsernameValidity',
|
||||||
createAsyncTypes('validateUsername'),
|
createAsyncTypes('validateUsername'),
|
||||||
createAsyncTypes('refetchCompletedChallenges'),
|
createAsyncTypes('refetchCompletedChallenges'),
|
||||||
@ -85,6 +86,14 @@ export const updateMyPortfolioError = createAction(
|
|||||||
export const deletePortfolio = createAction(types.deletePortfolio.start);
|
export const deletePortfolio = createAction(types.deletePortfolio.start);
|
||||||
export const deletePortfolioError = createAction(types.deletePortfolio.error);
|
export const deletePortfolioError = createAction(types.deletePortfolio.error);
|
||||||
|
|
||||||
|
export const updateMyProfileUI = createAction(types.updateMyProfileUI.start);
|
||||||
|
export const updateMyProfileUIComplete = createAction(
|
||||||
|
types.updateMyProfileUI.complete
|
||||||
|
);
|
||||||
|
export const updateMyProfileUIError = createAction(
|
||||||
|
types.updateMyProfileUI.error
|
||||||
|
);
|
||||||
|
|
||||||
export const resetProgress = createAction(types.resetProgress.start);
|
export const resetProgress = createAction(types.resetProgress.start);
|
||||||
export const resetProgressComplete = createAction(types.resetProgress.complete);
|
export const resetProgressComplete = createAction(types.resetProgress.complete);
|
||||||
export const resetProgressError = createAction(
|
export const resetProgressError = createAction(
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
types,
|
types,
|
||||||
refetchCompletedChallenges,
|
refetchCompletedChallenges,
|
||||||
updateUserBackendComplete,
|
updateUserBackendComplete,
|
||||||
updateMyPortfolioComplete
|
updateMyPortfolioComplete,
|
||||||
|
updateMyProfileUIComplete
|
||||||
} from './';
|
} from './';
|
||||||
import { makeToast } from '../../../Toasts/redux';
|
import { makeToast } from '../../../Toasts/redux';
|
||||||
import {
|
import {
|
||||||
@ -18,7 +19,8 @@ import {
|
|||||||
updateUserEmail,
|
updateUserEmail,
|
||||||
updateMultipleUserFlags,
|
updateMultipleUserFlags,
|
||||||
regresPortfolio,
|
regresPortfolio,
|
||||||
optoUpdatePortfolio
|
optoUpdatePortfolio,
|
||||||
|
updateLocalProfileUI
|
||||||
} from '../../../entities';
|
} from '../../../entities';
|
||||||
|
|
||||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
@ -188,9 +190,41 @@ function updateUserEmailEpic(actions, { getState }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMyProfileUIEpic(action$, { getState }) {
|
||||||
|
const toggle = action$::ofType(types.updateMyProfileUI.start);
|
||||||
|
|
||||||
|
const server = toggle.flatMap(({payload: { profileUI }}) => {
|
||||||
|
const state = getState();
|
||||||
|
const { csrfToken: _csrf } = state.app;
|
||||||
|
const username = usernameSelector(state);
|
||||||
|
const oldUI = { ...userSelector(state).profileUI };
|
||||||
|
return postJSON$('/update-my-profile-ui', { _csrf, profileUI })
|
||||||
|
.map(updateMyProfileUIComplete)
|
||||||
|
.catch(
|
||||||
|
doActionOnError(
|
||||||
|
() => Observable.of(
|
||||||
|
makeToast({
|
||||||
|
message:
|
||||||
|
'Something went wrong saving your privacy settings, ' +
|
||||||
|
'please try again.'
|
||||||
|
}),
|
||||||
|
updateLocalProfileUI({username, profileUI: oldUI })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const optimistic = toggle.flatMap(({payload: { profileUI }}) => {
|
||||||
|
const username = usernameSelector(getState());
|
||||||
|
return Observable.of(updateLocalProfileUI({username, profileUI}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Observable.merge(server, optimistic);
|
||||||
|
}
|
||||||
|
|
||||||
export default combineEpics(
|
export default combineEpics(
|
||||||
backendUserUpdateEpic,
|
backendUserUpdateEpic,
|
||||||
refetchCompletedChallengesEpic,
|
refetchCompletedChallengesEpic,
|
||||||
updateMyPortfolioEpic,
|
updateMyPortfolioEpic,
|
||||||
updateUserEmailEpic
|
updateUserEmailEpic,
|
||||||
|
updateMyProfileUIEpic
|
||||||
);
|
);
|
||||||
|
@ -63,6 +63,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privacy-settings {
|
||||||
|
|
||||||
|
.inline-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
.btn-group > label {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.@{ns}-email-container {
|
.@{ns}-email-container {
|
||||||
.below(sm, {
|
.below(sm, {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -747,6 +747,22 @@ module.exports = function(User) {
|
|||||||
`);
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
User.prototype.updateMyProfileUI = function updateMyProfileUI(profileUI) {
|
||||||
|
const oldUI = { ...this.profileUI };
|
||||||
|
const update = {
|
||||||
|
profileUI: {
|
||||||
|
...oldUI,
|
||||||
|
...profileUI
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.update$(update)
|
||||||
|
.do(() => Object.assign(this, update))
|
||||||
|
.map(() => dedent`
|
||||||
|
Your privacy settings have been updated.
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
User.prototype.updateMyUsername = function updateMyUsername(newUsername) {
|
User.prototype.updateMyUsername = function updateMyUsername(newUsername) {
|
||||||
return Observable.defer(
|
return Observable.defer(
|
||||||
() => {
|
() => {
|
||||||
|
@ -74,10 +74,6 @@
|
|||||||
},
|
},
|
||||||
"require": true
|
"require": true
|
||||||
},
|
},
|
||||||
"bio": {
|
|
||||||
"type": "string",
|
|
||||||
"default": ""
|
|
||||||
},
|
|
||||||
"about": {
|
"about": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": ""
|
"default": ""
|
||||||
@ -107,11 +103,6 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
"isLocked": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Campers profile does not show challenges/certificates to the public",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"currentChallengeId": {
|
"currentChallengeId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The challenge last visited by the user",
|
"description": "The challenge last visited by the user",
|
||||||
@ -210,6 +201,55 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "default"
|
"default": "default"
|
||||||
},
|
},
|
||||||
|
"profileUI": {
|
||||||
|
"type": {
|
||||||
|
"isLocked": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Campers profile shows only their username and avatar to the public",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"showAbout": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showCerts": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showHeatMap": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showLocation": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showName": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showPoints": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showPortfolio": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"showTimeLine": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For granular control of what is shown to the public",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"badges": {
|
"badges": {
|
||||||
"type": {
|
"type": {
|
||||||
"coreTeam": {
|
"coreTeam": {
|
||||||
|
@ -308,7 +308,6 @@ export default function certificate(app) {
|
|||||||
username,
|
username,
|
||||||
{
|
{
|
||||||
isCheater: true,
|
isCheater: true,
|
||||||
isLocked: true,
|
|
||||||
isFrontEndCert: true,
|
isFrontEndCert: true,
|
||||||
isBackEndCert: true,
|
isBackEndCert: true,
|
||||||
isFullStackCert: true,
|
isFullStackCert: true,
|
||||||
@ -322,11 +321,13 @@ export default function certificate(app) {
|
|||||||
isHonest: true,
|
isHonest: true,
|
||||||
username: true,
|
username: true,
|
||||||
name: true,
|
name: true,
|
||||||
completedChallenges: true
|
completedChallenges: true,
|
||||||
|
profileUI: true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
user => {
|
user => {
|
||||||
|
const { isLocked, showCerts } = user.profileUI;
|
||||||
const profile = `/portfolio/${user.username}`;
|
const profile = `/portfolio/${user.username}`;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
req.flash(
|
req.flash(
|
||||||
@ -341,7 +342,7 @@ export default function certificate(app) {
|
|||||||
'danger',
|
'danger',
|
||||||
dedent`
|
dedent`
|
||||||
This user needs to add their name to their account
|
This user needs to add their name to their account
|
||||||
in order for others to be able to view their certificate.
|
in order for others to be able to view their certification.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return res.redirect(profile);
|
return res.redirect(profile);
|
||||||
@ -351,13 +352,25 @@ export default function certificate(app) {
|
|||||||
return res.redirect(profile);
|
return res.redirect(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isLocked) {
|
if (isLocked) {
|
||||||
req.flash(
|
req.flash(
|
||||||
'danger',
|
'danger',
|
||||||
dedent`
|
dedent`
|
||||||
${username} has chosen to make their profile
|
${username} has chosen to make their profile
|
||||||
private. They will need to make their profile public
|
private. They will need to make their profile public
|
||||||
in order for others to be able to view their certificate.
|
in order for others to be able to view their certification.
|
||||||
|
`
|
||||||
|
);
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showCerts) {
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
dedent`
|
||||||
|
${username} has chosen to make their certifications
|
||||||
|
private. They will need to make their certifications public
|
||||||
|
in order for others to be able to view them.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
|
@ -114,6 +114,18 @@ export default function settingsController(app) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function updateMyProjects(req, res, next) {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
@ -164,6 +176,11 @@ export default function settingsController(app) {
|
|||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
updateMyPortfolio
|
updateMyPortfolio
|
||||||
);
|
);
|
||||||
|
api.post(
|
||||||
|
'/update-my-profile-ui',
|
||||||
|
ifNoUser401,
|
||||||
|
updateMyProfileUI
|
||||||
|
);
|
||||||
api.post(
|
api.post(
|
||||||
'/update-my-projects',
|
'/update-my-projects',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
|
@ -23,13 +23,13 @@ export const publicUserProps = [
|
|||||||
'isHonest',
|
'isHonest',
|
||||||
'isInfosecQaCert',
|
'isInfosecQaCert',
|
||||||
'isJsAlgoDataStructCert',
|
'isJsAlgoDataStructCert',
|
||||||
'isLocked',
|
|
||||||
'isRespWebDesignCert',
|
'isRespWebDesignCert',
|
||||||
'linkedin',
|
'linkedin',
|
||||||
'location',
|
'location',
|
||||||
'name',
|
'name',
|
||||||
'points',
|
'points',
|
||||||
'portfolio',
|
'portfolio',
|
||||||
|
'profileUI',
|
||||||
'projects',
|
'projects',
|
||||||
'streak',
|
'streak',
|
||||||
'twitter',
|
'twitter',
|
||||||
|
Reference in New Issue
Block a user