diff --git a/server/boot/settings.js b/server/boot/settings.js index f2d67628f4..64b695fab3 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -1,8 +1,5 @@ import { check } from 'express-validator/check'; -import { - ifNoUser401, - createValidatorErrorHandler -} from '../utils/middleware'; +import { ifNoUser401, createValidatorErrorHandler } from '../utils/middleware'; import { themes } from '../../common/utils/themes.js'; import { alertTypes } from '../../common/utils/flash.js'; @@ -10,25 +7,22 @@ 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({ + const currentValue = user[flag]; + return user.update$({ [flag]: !currentValue }).subscribe( + () => + res.status(200).json({ flag, value: !currentValue }), - next - ); + next + ); }; function refetchCompletedChallenges(req, res, next) { const { user } = req; - return user.requestCompletedChallenges() - .subscribe( - completedChallenges => res.json({ completedChallenges }), - next - ); + return user + .requestCompletedChallenges() + .subscribe(completedChallenges => res.json({ completedChallenges }), next); } const updateMyEmailValidators = [ @@ -38,12 +32,13 @@ export default function settingsController(app) { ]; function updateMyEmail(req, res, next) { - const { user, body: { email } } = req; - return user.requestUpdateEmail(email) - .subscribe( - message => res.json({ message }), - next - ); + const { + user, + body: { email } + } = req; + return user + .requestUpdateEmail(email) + .subscribe(message => res.json({ message }), next); } const updateMyCurrentChallengeValidators = [ @@ -53,48 +48,52 @@ export default function settingsController(app) { ]; function updateMyCurrentChallenge(req, res, next) { - const { user, body: { currentChallengeId } } = req; + const { + user, + body: { currentChallengeId } + } = req; return user.update$({ currentChallengeId }).subscribe( - () => res.json({ - message: - `your current challenge has been updated to ${currentChallengeId}` - }), + () => + res.json({ + message: `your current challenge has been updated to ${currentChallengeId}` + }), next ); } const updateMyThemeValidators = [ check('theme') - .isIn(Object.keys(themes)) - .withMessage('Theme is invalid.') + .isIn(Object.keys(themes)) + .withMessage('Theme is invalid.') ]; function updateMyTheme(req, res, next) { - const { body: { theme } } = req; + const { + body: { theme } + } = req; if (req.user.theme === theme) { return res.sendFlash(alertTypes.info, 'Theme already set'); } - return req.user.updateTheme(theme) - .then( - () => res.sendFlash(alertTypes.info, 'Your theme has been updated'), - next - ); + return req.user + .updateTheme(theme) + .then( + () => res.sendFlash(alertTypes.info, 'Your theme has been updated'), + next + ); } function updateFlags(req, res, next) { - const { user, body: { values } } = req; + const { + user, + body: { values } + } = req; const keys = Object.keys(values); - if ( - keys.length === 1 && - typeof keys[0] === 'boolean' - ) { + if (keys.length === 1 && typeof keys[0] === 'boolean') { return toggleUserFlag(keys[0], req, res, next); } - return user.requestUpdateFlags(values) - .subscribe( - message => res.json({ message }), - next - ); + return user + .requestUpdateFlags(values) + .subscribe(message => res.json({ message }), next); } function updateMyPortfolio(req, res, next) { @@ -106,23 +105,19 @@ export default function settingsController(app) { // user cannot send only one key to this route // other than to remove a portfolio item const requestDelete = Object.keys(portfolio).length === 1; - return user.updateMyPortfolio(portfolio, requestDelete) - .subscribe( - message => res.json({ message }), - next - ); - } + return user + .updateMyPortfolio(portfolio, requestDelete) + .subscribe(message => res.json({ message }), next); + } function updateMyProfileUI(req, res, next) { const { user, body: { profileUI } } = req; - return user.updateMyProfileUI(profileUI) - .subscribe( - message => res.json({ message }), - next - ); + return user + .updateMyProfileUI(profileUI) + .subscribe(message => res.json({ message }), next); } function updateMyProjects(req, res, next) { @@ -130,62 +125,56 @@ export default function settingsController(app) { user, body: { projects: project } } = req; - return user.updateMyProjects(project) - .subscribe( - message => res.json({ message }), - next - ); + return user + .updateMyProjects(project) + .subscribe(message => res.json({ message }), next); } function updateMyUsername(req, res, next) { - const { user, body: { username } } = req; - return user.updateMyUsername(username) - .subscribe( - message => res.json({ message }), - next - ); - } - - const updatePrivacyTerms = (req, res, next) => { const { user, - body: { quincyemails } + body: { username } + } = req; + return user + .updateMyUsername(username) + .subscribe(message => res.json({ message }), next); + } + + const updatePrivacyTerms = (req, res) => { + const { + user, + body: { quincyEmails } } = req; const update = { acceptedPrivacyTerms: true, - sendQuincyEmail: !!quincyemails + sendQuincyEmail: !!quincyEmails }; - return user.update$(update) - .do(() => { - req.user = Object.assign(req.user, update); - }) - .subscribe( - () => { - res.status(200).json({ - message: 'We have updated your preferences. ' + - 'You can now continue using freeCodeCamp.' - }); - }, - next - ); + return user.updateAttributes(update, err => { + if (err) { + return res.status(500).json({ + type: 'warning', + message: + 'Something went wrong updating your preferences. ' + + 'Please try again.' + }); + } + return res.status(200).json({ + type: 'success', + message: + 'We have updated your preferences. ' + + 'You can now continue using freeCodeCamp.' + }); + }); }; - api.post( - '/update-privacy-terms', - ifNoUser401, - updatePrivacyTerms - ); + api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms); api.post( '/refetch-user-completed-challenges', ifNoUser401, refetchCompletedChallenges ); - api.post( - '/update-flags', - ifNoUser401, - updateFlags - ); + api.post('/update-flags', ifNoUser401, updateFlags); api.post( '/update-my-email', ifNoUser401, @@ -207,21 +196,9 @@ export default function settingsController(app) { createValidatorErrorHandler(alertTypes.danger), 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-portfolio', ifNoUser401, updateMyPortfolio); + api.post('/update-my-profile-ui', ifNoUser401, updateMyProfileUI); + api.post('/update-my-projects', ifNoUser401, updateMyProjects); api.post( '/update-my-theme', ifNoUser401, @@ -229,11 +206,8 @@ export default function settingsController(app) { createValidatorErrorHandler(alertTypes.danger), updateMyTheme ); - api.post( - '/update-my-username', - ifNoUser401, - updateMyUsername - ); + api.post('/update-my-username', ifNoUser401, updateMyUsername); + app.use('/external', api); app.use(api); } diff --git a/src/components/Flash/flash.css b/src/components/Flash/flash.css new file mode 100644 index 0000000000..5cdb916d0a --- /dev/null +++ b/src/components/Flash/flash.css @@ -0,0 +1,6 @@ +.flash-message { + display: flex; + justify-content: space-around; + align-items: center; + flex-direction: row-reverse; +} diff --git a/src/components/Flash/index.js b/src/components/Flash/index.js new file mode 100644 index 0000000000..13917ad679 --- /dev/null +++ b/src/components/Flash/index.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Alert } from 'react-bootstrap'; + +import './flash.css'; + +function createDismissHandler(fn, id) { + return () => fn(id); +} + +function Flash({ messages, onClose }) { + return messages.map(({ type, message, id }) => ( + + {message} + + )); +} + +Flash.displayName = 'FlashMessages'; +Flash.propTypes = { + messages: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + type: PropTypes.string, + message: PropTypes.string + }) + ), + onClose: PropTypes.func.isRequired +}; + +export default Flash; diff --git a/src/components/Flash/redux/index.js b/src/components/Flash/redux/index.js new file mode 100644 index 0000000000..e71e5e1aac --- /dev/null +++ b/src/components/Flash/redux/index.js @@ -0,0 +1,36 @@ +import { createAction, handleActions } from 'redux-actions'; +import nanoId from 'nanoid'; + +import { createTypes } from '../../../utils/createTypes'; + +const ns = 'flash'; + +const initialState = { + messages: [] +}; + +const types = createTypes(['createFlashMessage', 'removeFlashMessage'], ns); + +export const sagas = []; + +export const createFlashMessage = createAction( + types.createFlashMessage, + msg => ({ id: nanoId(), ...msg }) +); +export const removeFlashMessage = createAction(types.removeFlashMessage); + +export const flashMessagesSelector = state => state[ns].messages; + +export const reducer = handleActions( + { + [types.createFlashMessage]: (state, { payload }) => ({ + ...state, + messages: [...state.messages, payload] + }), + [types.removeFlashMessage]: (state, { payload }) => ({ + ...state, + messages: state.messages.filter(msg => msg.id !== payload) + }) + }, + initialState +); diff --git a/src/components/layout.js b/src/components/layout.js index d631412bfb..4c0382e57d 100644 --- a/src/components/layout.js +++ b/src/components/layout.js @@ -7,16 +7,40 @@ import Helmet from 'react-helmet'; import { StaticQuery, graphql } from 'gatsby'; import { fetchUser, isSignedInSelector } from '../redux'; +import { flashMessagesSelector, removeFlashMessage } from './Flash/redux'; +import Flash from './Flash'; import Header from './Header'; import './layout.css'; import './global.css'; -const mapStateToProps = createSelector(isSignedInSelector, isSignedIn => ({ - isSignedIn -})); +const propTypes = { + children: PropTypes.node.isRequired, + disableSettings: PropTypes.bool, + fetchUser: PropTypes.func.isRequired, + flashMessages: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + type: PropTypes.string, + message: PropTypes.string + }) + ), + hasMessages: PropTypes.bool, + isSignedIn: PropTypes.bool, + removeFlashMessage: PropTypes.func.isRequired +}; + +const mapStateToProps = createSelector( + isSignedInSelector, + flashMessagesSelector, + (isSignedIn, flashMessages) => ({ + isSignedIn, + flashMessages, + hasMessages: !!flashMessages.length + }) +); const mapDispatchToProps = dispatch => - bindActionCreators({ fetchUser }, dispatch); + bindActionCreators({ fetchUser, removeFlashMessage }, dispatch); class Layout extends Component { constructor(props) { @@ -30,7 +54,13 @@ class Layout extends Component { } render() { - const { children, disableSettings } = this.props; + const { + children, + disableSettings, + hasMessages, + flashMessages = [], + removeFlashMessage + } = this.props; return (
-
{children}
+
+ {hasMessages ? ( + + ) : null} + {children} +
)} /> @@ -60,12 +95,7 @@ class Layout extends Component { } } -Layout.propTypes = { - children: PropTypes.node.isRequired, - disableSettings: PropTypes.bool, - fetchUser: PropTypes.func.isRequired, - isSignedIn: PropTypes.bool -}; +Layout.propTypes = propTypes; export default connect( mapStateToProps, diff --git a/src/pages/page-2.js b/src/pages/page-2.js deleted file mode 100644 index 4d3665d34c..0000000000 --- a/src/pages/page-2.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import { Link } from 'gatsby' - -import Layout from '../components/layout' - -const SecondPage = () => ( - -

Hi from the second page

-

Welcome to page 2

- Go back to the homepage -
-) - -export default SecondPage diff --git a/src/redux/rootReducer.js b/src/redux/rootReducer.js index b192fcb3b0..fb3e1b9832 100644 --- a/src/redux/rootReducer.js +++ b/src/redux/rootReducer.js @@ -1,7 +1,9 @@ import { combineReducers } from 'redux'; import { reducer as app } from './'; +import { reducer as flash } from '../components/Flash/redux'; export default combineReducers({ - app + app, + flash });