diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js
index 80964bc069..619f0416ff 100644
--- a/common/app/create-reducer.js
+++ b/common/app/create-reducer.js
@@ -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 })
});
}
diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js
index 75d921a777..435d3e93cd 100644
--- a/common/app/redux/actions.js
+++ b/common/app/redux/actions.js
@@ -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
diff --git a/common/app/redux/entities-reducer.js b/common/app/redux/entities-reducer.js
index 979fb7ca98..250ae395cc 100644
--- a/common/app/redux/entities-reducer.js
+++ b/common/app/redux/entities-reducer.js
@@ -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;
}
diff --git a/common/app/redux/types.js b/common/app/redux/types.js
index db6d159809..3ab9197c8b 100644
--- a/common/app/redux/types.js
+++ b/common/app/redux/types.js
@@ -7,6 +7,7 @@ export default createTypes([
'addUser',
'updateThisUser',
'updateUserPoints',
+ 'updateUserFlag',
'updateCompletedChallenges',
'showSignIn',
diff --git a/common/app/routes/settings/components/Email-Setting.jsx b/common/app/routes/settings/components/Email-Setting.jsx
index 418e86e145..a9332d56e4 100644
--- a/common/app/routes/settings/components/Email-Setting.jsx
+++ b/common/app/routes/settings/components/Email-Setting.jsx
@@ -10,6 +10,7 @@ export function UpdateEmailButton() {
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
+ href='/update-email'
>
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' }
@@ -84,6 +89,7 @@ export default function EmailSettings({
className={
classnames('positive-20', { active: sendNotificationEmail })
}
+ onClick={ toggleNotificationEmail }
>
{ sendNotificationEmail ? 'On' : 'Off' }
@@ -105,6 +111,7 @@ export default function EmailSettings({
className={
classnames('positive-20', { active: sendQuincyEmail })
}
+ onClick={ toggleQuincyEmail }
>
{ sendQuincyEmail ? 'On' : 'Off' }
@@ -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
};
diff --git a/common/app/routes/settings/components/Language-Settings.jsx b/common/app/routes/settings/components/Language-Settings.jsx
index 6f907803ff..f8b11aaec1 100644
--- a/common/app/routes/settings/components/Language-Settings.jsx
+++ b/common/app/routes/settings/components/Language-Settings.jsx
@@ -2,7 +2,15 @@ import React, { PropTypes } from 'react';
import { FormControl } from 'react-bootstrap';
import langs from '../../../../utils/supported-languages';
-const langOptions = [
+const options = [(
+
+ ),
...Object.keys(langs).map(tag => {
return (
- ),
- ...langOptions
- ];
return (
{ options }
diff --git a/common/app/routes/settings/components/Locked-Settings.jsx b/common/app/routes/settings/components/Locked-Settings.jsx
index e9dbee516f..077661d3f7 100644
--- a/common/app/routes/settings/components/Locked-Settings.jsx
+++ b/common/app/routes/settings/components/Locked-Settings.jsx
@@ -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' }
@@ -31,5 +32,6 @@ export default function LockSettings({ isLocked }) {
}
LockSettings.propTypes = {
- isLocked: PropTypes.bool
+ isLocked: PropTypes.bool,
+ toggle: PropTypes.func.isRequired
};
diff --git a/common/app/routes/settings/components/Settings.jsx b/common/app/routes/settings/components/Settings.jsx
index 0ca4ad9c2f..60adfe30b3 100644
--- a/common/app/routes/settings/components/Settings.jsx
+++ b/common/app/routes/settings/components/Settings.jsx
@@ -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 (
@@ -77,6 +141,7 @@ export default class Settings extends React.Component {
bsSize='lg'
bsStyle='primary'
className='btn-link-social'
+ onClick={ toggleNightMode }
>
NightMode
@@ -116,7 +181,10 @@ export default class Settings extends React.Component {
smOffset={ 2 }
xs={ 12 }
>
-
+
@@ -134,6 +202,9 @@ export default class Settings extends React.Component {
sendMonthlyEmail={ sendMonthlyEmail }
sendNotificationEmail={ sendNotificationEmail }
sendQuincyEmail={ sendQuincyEmail }
+ toggleMonthlyEmail={ toggleMonthlyEmail }
+ toggleNotificationEmail={ toggleNotificationEmail }
+ toggleQuincyEmail={ toggleQuincyEmail }
/>
@@ -160,7 +231,11 @@ export default class Settings extends React.Component {
smOffset={ 2 }
xs={ 12 }
>
-
+
@@ -168,3 +243,4 @@ export default class Settings extends React.Component {
}
}
+export default connect(mapStateToProps, actions)(Settings);
diff --git a/common/app/routes/settings/components/Social-Settings.jsx b/common/app/routes/settings/components/Social-Settings.jsx
index 53a9414d59..8d9139a888 100644
--- a/common/app/routes/settings/components/Social-Settings.jsx
+++ b/common/app/routes/settings/components/Social-Settings.jsx
@@ -45,7 +45,7 @@ export default function SocialSettings({
href='/link/linkedin'
key='linkedin'
>
-
+
Add my LinkedIn to my portfolio
));
diff --git a/common/app/routes/settings/redux/actions.js b/common/app/routes/settings/redux/actions.js
new file mode 100644
index 0000000000..9023184ae2
--- /dev/null
+++ b/common/app/routes/settings/redux/actions.js
@@ -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
+);
diff --git a/common/app/routes/settings/redux/index.js b/common/app/routes/settings/redux/index.js
new file mode 100644
index 0000000000..3d1b99889f
--- /dev/null
+++ b/common/app/routes/settings/redux/index.js
@@ -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
+];
diff --git a/common/app/routes/settings/redux/update-user-saga.js b/common/app/routes/settings/redux/update-user-saga.js
new file mode 100644
index 0000000000..2994a9c0ae
--- /dev/null
+++ b/common/app/routes/settings/redux/update-user-saga.js
@@ -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$);
+}
diff --git a/common/app/sagas.js b/common/app/sagas.js
index 3b668202f9..f5e1cbf011 100644
--- a/common/app/sagas.js
+++ b/common/app/sagas.js
@@ -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
];
diff --git a/server/boot/settings.js b/server/boot/settings.js
new file mode 100644
index 0000000000..680b4d8022
--- /dev/null
+++ b/server/boot/settings.js
@@ -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);
+}
diff --git a/server/boot/user.js b/server/boot/user.js
index b8c179e7fe..8c134e7a7c 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -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); }
diff --git a/server/services/user.js b/server/services/user.js
index 378bc6e00f..a094517ecb 100644
--- a/server/services/user.js
+++ b/server/services/user.js
@@ -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