Feature(settings): add user flag logic
This also moves a couple of settings to their own controller
This commit is contained in:
@ -8,6 +8,7 @@ import {
|
||||
reducer as challengesApp,
|
||||
projectNormalizer
|
||||
} from './routes/challenges/redux';
|
||||
import { reducer as settingsApp } from './routes/settings/redux';
|
||||
|
||||
export default function createReducer(sideReducers = {}) {
|
||||
return combineReducers({
|
||||
@ -16,6 +17,7 @@ export default function createReducer(sideReducers = {}) {
|
||||
app,
|
||||
toasts,
|
||||
challengesApp,
|
||||
settingsApp,
|
||||
form: formReducer.normalize({ ...projectNormalizer })
|
||||
});
|
||||
}
|
||||
|
@ -25,6 +25,11 @@ export const updateUserPoints = createAction(
|
||||
types.updateUserPoints,
|
||||
(username, points) => ({ username, points })
|
||||
);
|
||||
// updateUserPoints(username: String, flag: String) => Action
|
||||
export const updateUserFlag = createAction(
|
||||
types.updateUserFlag,
|
||||
(username, flag) => ({ username, flag })
|
||||
);
|
||||
// updateCompletedChallenges(username: String) => Action
|
||||
export const updateCompletedChallenges = createAction(
|
||||
types.updateCompletedChallenges
|
||||
|
@ -9,7 +9,7 @@ const initialState = {
|
||||
};
|
||||
|
||||
export default function entities(state = initialState, action) {
|
||||
const { type, payload: { username, points } = {} } = action;
|
||||
const { type, payload: { username, points, flag } = {} } = action;
|
||||
if (type === updateCompletedChallenges) {
|
||||
const username = action.payload;
|
||||
const completedChallengeMap = state.user[username].challengeMap || {};
|
||||
@ -44,5 +44,17 @@ export default function entities(state = initialState, action) {
|
||||
...action.meta.entities
|
||||
};
|
||||
}
|
||||
if (action.type === types.updateUserFlag) {
|
||||
return {
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
[username]: {
|
||||
...state.user[username],
|
||||
[flag]: !state.user[username][flag]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export default createTypes([
|
||||
'addUser',
|
||||
'updateThisUser',
|
||||
'updateUserPoints',
|
||||
'updateUserFlag',
|
||||
'updateCompletedChallenges',
|
||||
'showSignIn',
|
||||
|
||||
|
@ -10,6 +10,7 @@ export function UpdateEmailButton() {
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
className='btn-link-social'
|
||||
href='/update-email'
|
||||
>
|
||||
<FA name='envelope' />
|
||||
Update my Email
|
||||
@ -21,7 +22,10 @@ export default function EmailSettings({
|
||||
email,
|
||||
sendMonthlyEmail,
|
||||
sendNotificationEmail,
|
||||
sendQuincyEmail
|
||||
sendQuincyEmail,
|
||||
toggleMonthlyEmail,
|
||||
toggleNotificationEmail,
|
||||
toggleQuincyEmail
|
||||
}) {
|
||||
if (!email) {
|
||||
return (
|
||||
@ -63,6 +67,7 @@ export default function EmailSettings({
|
||||
className={
|
||||
classnames('positive-20', { active: sendMonthlyEmail })
|
||||
}
|
||||
onClick={ toggleMonthlyEmail }
|
||||
>
|
||||
{ sendMonthlyEmail ? 'On' : 'Off' }
|
||||
</Button>
|
||||
@ -84,6 +89,7 @@ export default function EmailSettings({
|
||||
className={
|
||||
classnames('positive-20', { active: sendNotificationEmail })
|
||||
}
|
||||
onClick={ toggleNotificationEmail }
|
||||
>
|
||||
{ sendNotificationEmail ? 'On' : 'Off' }
|
||||
</Button>
|
||||
@ -105,6 +111,7 @@ export default function EmailSettings({
|
||||
className={
|
||||
classnames('positive-20', { active: sendQuincyEmail })
|
||||
}
|
||||
onClick={ toggleQuincyEmail }
|
||||
>
|
||||
{ sendQuincyEmail ? 'On' : 'Off' }
|
||||
</Button>
|
||||
@ -118,5 +125,8 @@ EmailSettings.propTypes = {
|
||||
email: PropTypes.string,
|
||||
sendMonthlyEmail: PropTypes.bool,
|
||||
sendNotificationEmail: PropTypes.bool,
|
||||
sendQuincyEmail: PropTypes.bool
|
||||
sendQuincyEmail: PropTypes.bool,
|
||||
toggleMonthlyEmail: PropTypes.func.isRequired,
|
||||
toggleNotificationEmail: PropTypes.func.isRequired,
|
||||
toggleQuincyEmail: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -2,7 +2,15 @@ import React, { PropTypes } from 'react';
|
||||
import { FormControl } from 'react-bootstrap';
|
||||
import langs from '../../../../utils/supported-languages';
|
||||
|
||||
const langOptions = [
|
||||
const options = [(
|
||||
<option
|
||||
disabled={ true }
|
||||
key='default'
|
||||
value='not-the-momma'
|
||||
>
|
||||
Prefered Langauge
|
||||
</option>
|
||||
),
|
||||
...Object.keys(langs).map(tag => {
|
||||
return (
|
||||
<option
|
||||
@ -23,21 +31,12 @@ const langOptions = [
|
||||
];
|
||||
|
||||
export default function LangaugeSettings({ userLang }) {
|
||||
const options = [(
|
||||
<option
|
||||
disabled={ true }
|
||||
key='default'
|
||||
selected={ userLang ? false : true }
|
||||
>
|
||||
Prefered Langauge
|
||||
</option>
|
||||
),
|
||||
...langOptions
|
||||
];
|
||||
return (
|
||||
<FormControl
|
||||
className='btn btn-block btn-primary btn-link-social'
|
||||
componentClass='select'
|
||||
defaultValue='not-the-momma'
|
||||
value={ userLang }
|
||||
>
|
||||
{ options }
|
||||
</FormControl>
|
||||
|
@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default function LockSettings({ isLocked }) {
|
||||
export default function LockSettings({ isLocked, toggle }) {
|
||||
const className = classnames({
|
||||
'positive-20': true,
|
||||
active: isLocked
|
||||
@ -22,6 +22,7 @@ export default function LockSettings({ isLocked }) {
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
className={ className }
|
||||
onClick={ toggle }
|
||||
>
|
||||
{ isLocked ? 'On' : 'Off' }
|
||||
</Button>
|
||||
@ -31,5 +32,6 @@ export default function LockSettings({ isLocked }) {
|
||||
}
|
||||
|
||||
LockSettings.propTypes = {
|
||||
isLocked: PropTypes.bool
|
||||
isLocked: PropTypes.bool,
|
||||
toggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import FA from 'react-fontawesome';
|
||||
|
||||
@ -8,10 +9,58 @@ import EmailSettings from './Email-Setting.jsx';
|
||||
import LangaugeSettings from './Language-Settings.jsx';
|
||||
import DeleteModal from './Delete-Modal.jsx';
|
||||
|
||||
export default class Settings extends React.Component {
|
||||
import {
|
||||
toggleUserFlag,
|
||||
openDeleteModal,
|
||||
hideDeleteModal
|
||||
} from '../redux/actions';
|
||||
import { toggleNightMode } from '../../../redux/actions';
|
||||
|
||||
const actions = {
|
||||
toggleNightMode,
|
||||
openDeleteModal,
|
||||
hideDeleteModal,
|
||||
toggleIsLocked: () => toggleUserFlag('isLocked'),
|
||||
toggleQuincyEmail: () => toggleUserFlag('sendQuincyEmail'),
|
||||
toggleNotificationEmail: () => toggleUserFlag('sendNotificationEmail'),
|
||||
toggleMonthlyEmail: () => toggleUserFlag('sendMonthlyEmail')
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
app: { user: username },
|
||||
entities: { user: userMap },
|
||||
settingsApp: { isDeleteOpen }
|
||||
} = state;
|
||||
const {
|
||||
email,
|
||||
isLocked,
|
||||
isGithubCool,
|
||||
isTwitter,
|
||||
isLinkedIn,
|
||||
sendMonthlyEmail,
|
||||
sendNotificationEmail,
|
||||
sendQuincyEmail
|
||||
} = userMap[username] || {};
|
||||
return {
|
||||
username,
|
||||
email,
|
||||
isDeleteOpen,
|
||||
isLocked,
|
||||
isGithubCool,
|
||||
isTwitter,
|
||||
isLinkedIn,
|
||||
sendMonthlyEmail,
|
||||
sendNotificationEmail,
|
||||
sendQuincyEmail
|
||||
};
|
||||
};
|
||||
|
||||
export class Settings extends React.Component {
|
||||
static displayName = 'Settings';
|
||||
static propTypes = {
|
||||
username: PropTypes.string,
|
||||
isDeleteOpen: PropTypes.bool,
|
||||
isLocked: PropTypes.bool,
|
||||
isGithubCool: PropTypes.bool,
|
||||
isTwitter: PropTypes.bool,
|
||||
@ -19,12 +68,20 @@ export default class Settings extends React.Component {
|
||||
email: PropTypes.string,
|
||||
sendMonthlyEmail: PropTypes.bool,
|
||||
sendNotificationEmail: PropTypes.bool,
|
||||
sendQuincyEmail: PropTypes.bool
|
||||
sendQuincyEmail: PropTypes.bool,
|
||||
toggleNightMode: PropTypes.func,
|
||||
toggleIsLocked: PropTypes.func,
|
||||
toggleQuincyEmail: PropTypes.func,
|
||||
toggleMonthlyEmail: PropTypes.func,
|
||||
toggleNotificationEmail: PropTypes.func,
|
||||
openDeleteModal: PropTypes.func,
|
||||
hideDeleteModal: PropTypes.func
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
username,
|
||||
isDeleteOpen,
|
||||
isLocked,
|
||||
isGithubCool,
|
||||
isTwitter,
|
||||
@ -32,7 +89,14 @@ export default class Settings extends React.Component {
|
||||
email,
|
||||
sendMonthlyEmail,
|
||||
sendNotificationEmail,
|
||||
sendQuincyEmail
|
||||
sendQuincyEmail,
|
||||
toggleNightMode,
|
||||
toggleIsLocked,
|
||||
toggleQuincyEmail,
|
||||
toggleMonthlyEmail,
|
||||
toggleNotificationEmail,
|
||||
openDeleteModal,
|
||||
hideDeleteModal
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
@ -77,6 +141,7 @@ export default class Settings extends React.Component {
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
className='btn-link-social'
|
||||
onClick={ toggleNightMode }
|
||||
>
|
||||
NightMode
|
||||
</Button>
|
||||
@ -116,7 +181,10 @@ export default class Settings extends React.Component {
|
||||
smOffset={ 2 }
|
||||
xs={ 12 }
|
||||
>
|
||||
<LockedSettings isLocked={ isLocked } />
|
||||
<LockedSettings
|
||||
isLocked={ isLocked }
|
||||
toggle={ toggleIsLocked }
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
@ -134,6 +202,9 @@ export default class Settings extends React.Component {
|
||||
sendMonthlyEmail={ sendMonthlyEmail }
|
||||
sendNotificationEmail={ sendNotificationEmail }
|
||||
sendQuincyEmail={ sendQuincyEmail }
|
||||
toggleMonthlyEmail={ toggleMonthlyEmail }
|
||||
toggleNotificationEmail={ toggleNotificationEmail }
|
||||
toggleQuincyEmail={ toggleQuincyEmail }
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -160,7 +231,11 @@ export default class Settings extends React.Component {
|
||||
smOffset={ 2 }
|
||||
xs={ 12 }
|
||||
>
|
||||
<DeleteModal />
|
||||
<DeleteModal
|
||||
hide={ hideDeleteModal }
|
||||
isOpen={ isDeleteOpen }
|
||||
open={ openDeleteModal }
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
@ -168,3 +243,4 @@ export default class Settings extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, actions)(Settings);
|
||||
|
@ -45,7 +45,7 @@ export default function SocialSettings({
|
||||
href='/link/linkedin'
|
||||
key='linkedin'
|
||||
>
|
||||
<FA name='linked' />
|
||||
<FA name='linkedin' />
|
||||
Add my LinkedIn to my portfolio
|
||||
</Button>
|
||||
));
|
||||
|
30
common/app/routes/settings/redux/actions.js
Normal file
30
common/app/routes/settings/redux/actions.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { createAction, handleActions } from 'redux-actions';
|
||||
|
||||
import createTypes from '../../../utils/create-types';
|
||||
|
||||
const initialState = {
|
||||
showDeleteModal: false
|
||||
};
|
||||
export const types = createTypes([
|
||||
'toggleUserFlag',
|
||||
'openDeleteModal',
|
||||
'hideDeleteModal'
|
||||
], 'settings');
|
||||
|
||||
export const toggleUserFlag = createAction(types.toggleUserFlag);
|
||||
export const openDeleteModal = createAction(types.openDeleteModal);
|
||||
export const hideDeleteModal = createAction(types.hideDeleteModal);
|
||||
|
||||
export default handleActions(
|
||||
{
|
||||
[openDeleteModal]: state => ({
|
||||
...state,
|
||||
isDeleteOpen: true
|
||||
}),
|
||||
[hideDeleteModal]: state => ({
|
||||
...state,
|
||||
isDeleteOpen: false
|
||||
})
|
||||
},
|
||||
initialState
|
||||
);
|
9
common/app/routes/settings/redux/index.js
Normal file
9
common/app/routes/settings/redux/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import userUpdateSaga from './update-user-saga';
|
||||
|
||||
export { types } from './actions';
|
||||
export * as actions from './actions';
|
||||
export { default as reducer } from './actions';
|
||||
|
||||
export const sagas = [
|
||||
userUpdateSaga
|
||||
];
|
40
common/app/routes/settings/redux/update-user-saga.js
Normal file
40
common/app/routes/settings/redux/update-user-saga.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { Observable } from 'rx';
|
||||
import { types } from './actions';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
import { updateUserFlag, createErrorObservable } from '../../../redux/actions';
|
||||
|
||||
const urlMap = {
|
||||
isLocked: 'lockdown',
|
||||
sendQuincyEmail: 'quincy-email',
|
||||
sendNotificationEmail: 'notification-email',
|
||||
sendMonthlyEmail: 'announcement-email'
|
||||
};
|
||||
export default function userUpdateSaga(actions$, getState) {
|
||||
const toggleFlag$ = actions$
|
||||
.filter(({ type, payload }) => type === types.toggleUserFlag && payload)
|
||||
.map(({ payload }) => payload);
|
||||
const optimistic$ = toggleFlag$.map(flag => {
|
||||
const { app: { user: username } } = getState();
|
||||
return updateUserFlag(username, flag);
|
||||
});
|
||||
const serverUpdate$ = toggleFlag$
|
||||
.debounce(500)
|
||||
.flatMap(flag => {
|
||||
const url = `/toggle-${urlMap[ flag ]}`;
|
||||
const {
|
||||
app: { user: username, csrfToken: _csrf },
|
||||
entities: { user: userMap }
|
||||
} = getState();
|
||||
const user = userMap[username];
|
||||
const currentValue = user[ flag ];
|
||||
return postJSON$(url, { _csrf })
|
||||
.map(({ flag, value }) => {
|
||||
if (currentValue === value) {
|
||||
return null;
|
||||
}
|
||||
return updateUserFlag(username, flag);
|
||||
})
|
||||
.catch(createErrorObservable);
|
||||
});
|
||||
return Observable.merge(optimistic$, serverUpdate$);
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { sagas as appSagas } from './redux';
|
||||
import { sagas as challengeSagas } from './routes/challenges/redux';
|
||||
import { sagas as settingsSagas } from './routes/settings/redux';
|
||||
|
||||
export default [
|
||||
...appSagas,
|
||||
...challengeSagas
|
||||
...challengeSagas,
|
||||
...settingsSagas
|
||||
];
|
||||
|
38
server/boot/settings.js
Normal file
38
server/boot/settings.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { ifNoUser401 } from '../utils/middleware';
|
||||
|
||||
export default function settingsController(app) {
|
||||
const api = app.loopback.Router();
|
||||
const toggleUserFlag = flag => (req, res, next) => {
|
||||
const { user } = req;
|
||||
const currentValue = user[ flag ];
|
||||
return user
|
||||
.update$({ [ flag ]: !currentValue })
|
||||
.subscribe(
|
||||
() => res.status(200).json({
|
||||
flag,
|
||||
value: !currentValue
|
||||
}),
|
||||
next
|
||||
);
|
||||
};
|
||||
api.post(
|
||||
'/toggle-lockdown',
|
||||
toggleUserFlag('isLocked')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-announcement-email',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendMonthlyEmail')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-notification-email',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendNotificationEmail')
|
||||
);
|
||||
api.post(
|
||||
'/toggle-quincy-email',
|
||||
ifNoUser401,
|
||||
toggleUserFlag('sendQuincyEmail')
|
||||
);
|
||||
app.use(api);
|
||||
}
|
@ -155,26 +155,6 @@ module.exports = function(app) {
|
||||
router.get('/email-signin', getEmailSignin);
|
||||
router.get('/deprecated-signin', getDepSignin);
|
||||
router.get('/update-email', getUpdateEmail);
|
||||
api.get(
|
||||
'/toggle-lockdown-mode',
|
||||
sendNonUserToMap,
|
||||
toggleLockdownMode
|
||||
);
|
||||
api.get(
|
||||
'/toggle-announcement-email-mode',
|
||||
sendNonUserToMap,
|
||||
toggleReceivesAnnouncementEmails
|
||||
);
|
||||
api.get(
|
||||
'/toggle-notification-email-mode',
|
||||
sendNonUserToMap,
|
||||
toggleReceivesNotificationEmails
|
||||
);
|
||||
api.get(
|
||||
'/toggle-quincy-email-mode',
|
||||
sendNonUserToMap,
|
||||
toggleReceivesQuincyEmails
|
||||
);
|
||||
api.post(
|
||||
'/account/delete',
|
||||
ifNoUser401,
|
||||
@ -434,62 +414,6 @@ module.exports = function(app) {
|
||||
);
|
||||
}
|
||||
|
||||
function toggleLockdownMode(req, res, next) {
|
||||
const { user } = req;
|
||||
user.update$({ isLocked: !user.isLocked })
|
||||
.subscribe(
|
||||
() => {
|
||||
req.flash('info', {
|
||||
msg: 'We\'ve successfully updated your Privacy preferences.'
|
||||
});
|
||||
return res.redirect('/settings');
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function toggleReceivesAnnouncementEmails(req, res, next) {
|
||||
const { user } = req;
|
||||
return user.update$({ sendMonthlyEmail: !user.sendMonthlyEmail })
|
||||
.subscribe(
|
||||
() => {
|
||||
req.flash('info', {
|
||||
msg: 'We\'ve successfully updated your Email preferences.'
|
||||
});
|
||||
return res.redirect('/settings');
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function toggleReceivesQuincyEmails(req, res, next) {
|
||||
const { user } = req;
|
||||
return user.update$({ sendQuincyEmail: !user.sendQuincyEmail })
|
||||
.subscribe(
|
||||
() => {
|
||||
req.flash('info', {
|
||||
msg: 'We\'ve successfully updated your Email preferences.'
|
||||
});
|
||||
return res.redirect('/settings');
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function toggleReceivesNotificationEmails(req, res, next) {
|
||||
const { user } = req;
|
||||
return user.update$({ sendNotificationEmail: !user.sendNotificationEmail })
|
||||
.subscribe(
|
||||
() => {
|
||||
req.flash('info', {
|
||||
msg: 'We\'ve successfully updated your Email preferences.'
|
||||
});
|
||||
return res.redirect('/settings');
|
||||
},
|
||||
next
|
||||
);
|
||||
}
|
||||
|
||||
function postDeleteAccount(req, res, next) {
|
||||
User.destroyById(req.user.id, function(err) {
|
||||
if (err) { return next(err); }
|
||||
|
@ -9,17 +9,23 @@ const publicUserProps = [
|
||||
'theme',
|
||||
'picture',
|
||||
'points',
|
||||
'email',
|
||||
'languageTag',
|
||||
|
||||
'isCheater',
|
||||
'isGithubCool',
|
||||
|
||||
'isLocked',
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert',
|
||||
'isFullStackCert',
|
||||
|
||||
'githubURL',
|
||||
'sendMonthlyEmail',
|
||||
'sendNotificationEmail',
|
||||
'sendQuincyEmail',
|
||||
|
||||
'currentChallenge',
|
||||
'challengeMap'
|
||||
];
|
||||
@ -40,7 +46,11 @@ export default function userServices() {
|
||||
{
|
||||
entities: {
|
||||
user: {
|
||||
[user.username]: _.pick(user, publicUserProps)
|
||||
[user.username]: {
|
||||
..._.pick(user, publicUserProps),
|
||||
isTwitter: !!user.twitter,
|
||||
isLinkedIn: !!user.linkedIn
|
||||
}
|
||||
}
|
||||
},
|
||||
result: user.username
|
||||
|
Reference in New Issue
Block a user