feat(flash): Add flash messages

This commit is contained in:
Bouncey
2018-08-25 00:17:40 +01:00
committed by mrugesh mohapatra
parent 3ea38ee31a
commit 4f2241d39f
7 changed files with 212 additions and 142 deletions

View File

@ -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);
}

View File

@ -0,0 +1,6 @@
.flash-message {
display: flex;
justify-content: space-around;
align-items: center;
flex-direction: row-reverse;
}

View File

@ -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 }) => (
<Alert
bsStyle={type}
className='flash-message'
key={id}
onDismiss={createDismissHandler(onClose, id)}
>
{message}
</Alert>
));
}
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;

View File

@ -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
);

View File

@ -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 (
<StaticQuery
query={graphql`
@ -52,7 +82,12 @@ class Layout extends Component {
title={data.site.siteMetadata.title}
/>
<Header disableSettings={disableSettings} />
<div style={{ marginTop: '38px' }}>{children}</div>
<div style={{ marginTop: '38px' }}>
{hasMessages ? (
<Flash messages={flashMessages} onClose={removeFlashMessage} />
) : null}
{children}
</div>
</Fragment>
)}
/>
@ -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,

View File

@ -1,14 +0,0 @@
import React from 'react'
import { Link } from 'gatsby'
import Layout from '../components/layout'
const SecondPage = () => (
<Layout>
<h1>Hi from the second page</h1>
<p>Welcome to page 2</p>
<Link to="/">Go back to the homepage</Link>
</Layout>
)
export default SecondPage

View File

@ -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
});