diff --git a/gatsby-browser.js b/gatsby-browser.js index 72217801ec..c0b032c1e0 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -3,11 +3,16 @@ import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import { createStore } from './src/redux/createStore'; +import AppMountNotifier from './src/components/AppMountNotifier'; const store = createStore(); export const wrapRootElement = ({ element }) => { - return {element}; + return ( + + element} /> + + ); }; wrapRootElement.propTypes = { diff --git a/server/middlewares/error-handlers.js b/server/middlewares/error-handlers.js index 99f6ecb39f..cee4d89ac6 100644 --- a/server/middlewares/error-handlers.js +++ b/server/middlewares/error-handlers.js @@ -69,22 +69,10 @@ export default function prodErrorHandler() { } if (type === 'html') { - if (isDev) { - return res.render( - 'dev-error', - { - ...handled, - stack: createStackHtml(err), - errorTitle: createErrorTitle(err), - title: 'freeCodeCamp - Server Error', - status - } - ); - } if (typeof req.flash === 'function') { req.flash(handled.type || 'danger', message); } - return res.redirect(redirectTo); + return res.redirectWithFlash(redirectTo); // json } else if (type === 'json') { res.setHeader('Content-Type', 'application/json'); diff --git a/server/middlewares/express-extensions.js b/server/middlewares/express-extensions.js index 69b4cce108..30b523ac3c 100644 --- a/server/middlewares/express-extensions.js +++ b/server/middlewares/express-extensions.js @@ -1,14 +1,17 @@ -import { Observable } from 'rx'; +import qs from 'query-string'; // add rx methods to express export default function() { return function expressExtensions(req, res, next) { - // express flash will overwrite render with one that will - // dump flash messages to locals on every call to render - // Use this when that behavior is not wanted - res.renderWithoutFlash = res.render; - // render to observable stream using build in render - res.render$ = Observable.fromNodeCallback(res.render, res); + res.redirectWithFlash = uri => { + const flash = req.flash(); + res.redirect( + `${uri}?${qs.stringify( + { messages: qs.stringify(flash, { arrayFormat: 'index' }) }, + { arrayFormat: 'index' } + )}` + ); + }; res.sendFlash = (type, message) => { if (type && message) { req.flash(type, message); diff --git a/src/components/AppMountNotifier.js b/src/components/AppMountNotifier.js new file mode 100644 index 0000000000..5d921c577c --- /dev/null +++ b/src/components/AppMountNotifier.js @@ -0,0 +1,30 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import { appMount } from '../redux'; + +const mapStateToProps = () => ({}); +const mapDispatchToProps = dispatch => + bindActionCreators({ appMount }, dispatch); + +class AppMountNotifer extends Component { + componentDidMount() { + return this.props.appMount(); + } + render() { + return this.props.render(); + } +} + +AppMountNotifer.displayName = 'AppMountNotifier'; +AppMountNotifer.propTypes = { + appMount: PropTypes.func.isRequired, + render: PropTypes.func.isRequired +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(AppMountNotifer); diff --git a/src/components/Flash/flash.css b/src/components/Flash/flash.css index 5cdb916d0a..51c5e401d1 100644 --- a/src/components/Flash/flash.css +++ b/src/components/Flash/flash.css @@ -3,4 +3,5 @@ justify-content: space-around; align-items: center; flex-direction: row-reverse; + margin-bottom: 0px; } diff --git a/src/components/Flash/index.js b/src/components/Flash/index.js index 13917ad679..5a0cb9c531 100644 --- a/src/components/Flash/index.js +++ b/src/components/Flash/index.js @@ -16,7 +16,7 @@ function Flash({ messages, onClose }) { key={id} onDismiss={createDismissHandler(onClose, id)} > - {message} +
)); } diff --git a/src/redux/app-mount-saga.js b/src/redux/app-mount-saga.js new file mode 100644 index 0000000000..d5f2774f18 --- /dev/null +++ b/src/redux/app-mount-saga.js @@ -0,0 +1,28 @@ +import { put, takeEvery } from 'redux-saga/effects'; +import qs from 'query-string'; +import { createFlashMessage } from '../components/Flash/redux'; + +function* parseMessagesSaga() { + const search = window.location.search.slice(); + // TODO(Bouncey): Find a way to clear the search with causing a re-render + if (search) { + const { messages } = qs.parse(search, { arrayFormat: 'index' }); + if (messages) { + const flashMap = qs.parse(messages, { arrayFormat: 'index' }); + const flash = Object.keys(flashMap).reduce( + (acc, type) => [ + ...acc, + ...flashMap[type].map(message => ({ type, message })) + ], + [] + ); + for (let i = 0; i < flash.length; i++) { + yield put(createFlashMessage(flash[i])); + } + } + } +} + +export function createAppMountSaga(types) { + return [takeEvery(types.appMount, parseMessagesSaga)]; +} diff --git a/src/redux/index.js b/src/redux/index.js index 4d5c50fa99..e03baa6840 100644 --- a/src/redux/index.js +++ b/src/redux/index.js @@ -3,6 +3,8 @@ import { createAction, handleActions } from 'redux-actions'; import { createTypes, createAsyncTypes } from '../utils/createTypes'; import { createFetchUserSaga } from './fetch-user-saga'; import { createAcceptTermsSaga } from './accept-terms-saga'; +import { createAppMountSaga } from './app-mount-saga'; +import { createUpdateMyEmailSaga } from './update-email-saga'; const ns = 'app'; @@ -18,22 +20,35 @@ const initialState = { }; const types = createTypes( - [...createAsyncTypes('fetchUser'), ...createAsyncTypes('acceptTerms')], + [ + 'appMount', + ...createAsyncTypes('fetchUser'), + ...createAsyncTypes('acceptTerms'), + ...createAsyncTypes('updateMyEmail') + ], ns ); export const sagas = [ + ...createAcceptTermsSaga(types), + ...createAppMountSaga(types), ...createFetchUserSaga(types), - ...createAcceptTermsSaga(types) + ...createUpdateMyEmailSaga(types) ]; +export const appMount = createAction(types.appMount); + +export const acceptTerms = createAction(types.acceptTerms); +export const acceptTermsComplete = createAction(types.acceptTermsComplete); +export const acceptTermsError = createAction(types.acceptTermsError); + export const fetchUser = createAction(types.fetchUser); export const fetchUserComplete = createAction(types.fetchUserComplete); export const fetchUserError = createAction(types.fetchUserError); -export const acceptTerms = createAction(types.acceptTerms); -export const acceptTermsComplete = createAction(types.acceptTermsComplete); -export const acceptTermsError = createAction(types.acceptTermsError); +export const updateMyEmail = createAction(types.updateMyEmail); +export const updateMyEmailComplete = createAction(types.updateMyEmailComplete); +export const updateMyEmailError = createAction(types.updateMyEmailError); export const isSignedInSelector = state => !!Object.keys(state[ns].user).length; export const userFetchStateSelector = state => state[ns].fetchState;