feat(flash): Add flash messages
This commit is contained in:
committed by
mrugesh mohapatra
parent
3ea38ee31a
commit
4f2241d39f
@ -1,8 +1,5 @@
|
|||||||
import { check } from 'express-validator/check';
|
import { check } from 'express-validator/check';
|
||||||
import {
|
import { ifNoUser401, createValidatorErrorHandler } from '../utils/middleware';
|
||||||
ifNoUser401,
|
|
||||||
createValidatorErrorHandler
|
|
||||||
} from '../utils/middleware';
|
|
||||||
import { themes } from '../../common/utils/themes.js';
|
import { themes } from '../../common/utils/themes.js';
|
||||||
import { alertTypes } from '../../common/utils/flash.js';
|
import { alertTypes } from '../../common/utils/flash.js';
|
||||||
|
|
||||||
@ -10,25 +7,22 @@ export default function settingsController(app) {
|
|||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const toggleUserFlag = (flag, req, res, next) => {
|
const toggleUserFlag = (flag, req, res, next) => {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const currentValue = user[ flag ];
|
const currentValue = user[flag];
|
||||||
return user
|
return user.update$({ [flag]: !currentValue }).subscribe(
|
||||||
.update$({ [ flag ]: !currentValue })
|
() =>
|
||||||
.subscribe(
|
res.status(200).json({
|
||||||
() => res.status(200).json({
|
|
||||||
flag,
|
flag,
|
||||||
value: !currentValue
|
value: !currentValue
|
||||||
}),
|
}),
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function refetchCompletedChallenges(req, res, next) {
|
function refetchCompletedChallenges(req, res, next) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
return user.requestCompletedChallenges()
|
return user
|
||||||
.subscribe(
|
.requestCompletedChallenges()
|
||||||
completedChallenges => res.json({ completedChallenges }),
|
.subscribe(completedChallenges => res.json({ completedChallenges }), next);
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateMyEmailValidators = [
|
const updateMyEmailValidators = [
|
||||||
@ -38,12 +32,13 @@ export default function settingsController(app) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
function updateMyEmail(req, res, next) {
|
function updateMyEmail(req, res, next) {
|
||||||
const { user, body: { email } } = req;
|
const {
|
||||||
return user.requestUpdateEmail(email)
|
user,
|
||||||
.subscribe(
|
body: { email }
|
||||||
message => res.json({ message }),
|
} = req;
|
||||||
next
|
return user
|
||||||
);
|
.requestUpdateEmail(email)
|
||||||
|
.subscribe(message => res.json({ message }), next);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateMyCurrentChallengeValidators = [
|
const updateMyCurrentChallengeValidators = [
|
||||||
@ -53,48 +48,52 @@ export default function settingsController(app) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
function updateMyCurrentChallenge(req, res, next) {
|
function updateMyCurrentChallenge(req, res, next) {
|
||||||
const { user, body: { currentChallengeId } } = req;
|
const {
|
||||||
|
user,
|
||||||
|
body: { currentChallengeId }
|
||||||
|
} = req;
|
||||||
return user.update$({ currentChallengeId }).subscribe(
|
return user.update$({ currentChallengeId }).subscribe(
|
||||||
() => res.json({
|
() =>
|
||||||
message:
|
res.json({
|
||||||
`your current challenge has been updated to ${currentChallengeId}`
|
message: `your current challenge has been updated to ${currentChallengeId}`
|
||||||
}),
|
}),
|
||||||
next
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateMyThemeValidators = [
|
const updateMyThemeValidators = [
|
||||||
check('theme')
|
check('theme')
|
||||||
.isIn(Object.keys(themes))
|
.isIn(Object.keys(themes))
|
||||||
.withMessage('Theme is invalid.')
|
.withMessage('Theme is invalid.')
|
||||||
];
|
];
|
||||||
|
|
||||||
function updateMyTheme(req, res, next) {
|
function updateMyTheme(req, res, next) {
|
||||||
const { body: { theme } } = req;
|
const {
|
||||||
|
body: { theme }
|
||||||
|
} = req;
|
||||||
if (req.user.theme === theme) {
|
if (req.user.theme === theme) {
|
||||||
return res.sendFlash(alertTypes.info, 'Theme already set');
|
return res.sendFlash(alertTypes.info, 'Theme already set');
|
||||||
}
|
}
|
||||||
return req.user.updateTheme(theme)
|
return req.user
|
||||||
.then(
|
.updateTheme(theme)
|
||||||
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
|
.then(
|
||||||
next
|
() => res.sendFlash(alertTypes.info, 'Your theme has been updated'),
|
||||||
);
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFlags(req, res, next) {
|
function updateFlags(req, res, next) {
|
||||||
const { user, body: { values } } = req;
|
const {
|
||||||
|
user,
|
||||||
|
body: { values }
|
||||||
|
} = req;
|
||||||
const keys = Object.keys(values);
|
const keys = Object.keys(values);
|
||||||
if (
|
if (keys.length === 1 && typeof keys[0] === 'boolean') {
|
||||||
keys.length === 1 &&
|
|
||||||
typeof keys[0] === 'boolean'
|
|
||||||
) {
|
|
||||||
return toggleUserFlag(keys[0], req, res, next);
|
return toggleUserFlag(keys[0], req, res, next);
|
||||||
}
|
}
|
||||||
return user.requestUpdateFlags(values)
|
return user
|
||||||
.subscribe(
|
.requestUpdateFlags(values)
|
||||||
message => res.json({ message }),
|
.subscribe(message => res.json({ message }), next);
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMyPortfolio(req, res, next) {
|
function updateMyPortfolio(req, res, next) {
|
||||||
@ -106,23 +105,19 @@ export default function settingsController(app) {
|
|||||||
// user cannot send only one key to this route
|
// user cannot send only one key to this route
|
||||||
// other than to remove a portfolio item
|
// other than to remove a portfolio item
|
||||||
const requestDelete = Object.keys(portfolio).length === 1;
|
const requestDelete = Object.keys(portfolio).length === 1;
|
||||||
return user.updateMyPortfolio(portfolio, requestDelete)
|
return user
|
||||||
.subscribe(
|
.updateMyPortfolio(portfolio, requestDelete)
|
||||||
message => res.json({ message }),
|
.subscribe(message => res.json({ message }), next);
|
||||||
next
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMyProfileUI(req, res, next) {
|
function updateMyProfileUI(req, res, next) {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
body: { profileUI }
|
body: { profileUI }
|
||||||
} = req;
|
} = req;
|
||||||
return user.updateMyProfileUI(profileUI)
|
return user
|
||||||
.subscribe(
|
.updateMyProfileUI(profileUI)
|
||||||
message => res.json({ message }),
|
.subscribe(message => res.json({ message }), next);
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMyProjects(req, res, next) {
|
function updateMyProjects(req, res, next) {
|
||||||
@ -130,62 +125,56 @@ export default function settingsController(app) {
|
|||||||
user,
|
user,
|
||||||
body: { projects: project }
|
body: { projects: project }
|
||||||
} = req;
|
} = req;
|
||||||
return user.updateMyProjects(project)
|
return user
|
||||||
.subscribe(
|
.updateMyProjects(project)
|
||||||
message => res.json({ message }),
|
.subscribe(message => res.json({ message }), next);
|
||||||
next
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMyUsername(req, res, 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 {
|
const {
|
||||||
user,
|
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;
|
} = req;
|
||||||
const update = {
|
const update = {
|
||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail: !!quincyemails
|
sendQuincyEmail: !!quincyEmails
|
||||||
};
|
};
|
||||||
return user.update$(update)
|
return user.updateAttributes(update, err => {
|
||||||
.do(() => {
|
if (err) {
|
||||||
req.user = Object.assign(req.user, update);
|
return res.status(500).json({
|
||||||
})
|
type: 'warning',
|
||||||
.subscribe(
|
message:
|
||||||
() => {
|
'Something went wrong updating your preferences. ' +
|
||||||
res.status(200).json({
|
'Please try again.'
|
||||||
message: 'We have updated your preferences. ' +
|
});
|
||||||
'You can now continue using freeCodeCamp.'
|
}
|
||||||
});
|
return res.status(200).json({
|
||||||
},
|
type: 'success',
|
||||||
next
|
message:
|
||||||
);
|
'We have updated your preferences. ' +
|
||||||
|
'You can now continue using freeCodeCamp.'
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
api.post(
|
api.put('/update-privacy-terms', ifNoUser401, updatePrivacyTerms);
|
||||||
'/update-privacy-terms',
|
|
||||||
ifNoUser401,
|
|
||||||
updatePrivacyTerms
|
|
||||||
);
|
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/refetch-user-completed-challenges',
|
'/refetch-user-completed-challenges',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
refetchCompletedChallenges
|
refetchCompletedChallenges
|
||||||
);
|
);
|
||||||
api.post(
|
api.post('/update-flags', ifNoUser401, updateFlags);
|
||||||
'/update-flags',
|
|
||||||
ifNoUser401,
|
|
||||||
updateFlags
|
|
||||||
);
|
|
||||||
api.post(
|
api.post(
|
||||||
'/update-my-email',
|
'/update-my-email',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -207,21 +196,9 @@ export default function settingsController(app) {
|
|||||||
createValidatorErrorHandler(alertTypes.danger),
|
createValidatorErrorHandler(alertTypes.danger),
|
||||||
updateMyCurrentChallenge
|
updateMyCurrentChallenge
|
||||||
);
|
);
|
||||||
api.post(
|
api.post('/update-my-portfolio', ifNoUser401, updateMyPortfolio);
|
||||||
'/update-my-portfolio',
|
api.post('/update-my-profile-ui', ifNoUser401, updateMyProfileUI);
|
||||||
ifNoUser401,
|
api.post('/update-my-projects', ifNoUser401, updateMyProjects);
|
||||||
updateMyPortfolio
|
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/update-my-profile-ui',
|
|
||||||
ifNoUser401,
|
|
||||||
updateMyProfileUI
|
|
||||||
);
|
|
||||||
api.post(
|
|
||||||
'/update-my-projects',
|
|
||||||
ifNoUser401,
|
|
||||||
updateMyProjects
|
|
||||||
);
|
|
||||||
api.post(
|
api.post(
|
||||||
'/update-my-theme',
|
'/update-my-theme',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -229,11 +206,8 @@ export default function settingsController(app) {
|
|||||||
createValidatorErrorHandler(alertTypes.danger),
|
createValidatorErrorHandler(alertTypes.danger),
|
||||||
updateMyTheme
|
updateMyTheme
|
||||||
);
|
);
|
||||||
api.post(
|
api.post('/update-my-username', ifNoUser401, updateMyUsername);
|
||||||
'/update-my-username',
|
|
||||||
ifNoUser401,
|
|
||||||
updateMyUsername
|
|
||||||
);
|
|
||||||
|
|
||||||
|
app.use('/external', api);
|
||||||
app.use(api);
|
app.use(api);
|
||||||
}
|
}
|
||||||
|
6
src/components/Flash/flash.css
Normal file
6
src/components/Flash/flash.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.flash-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
36
src/components/Flash/index.js
Normal file
36
src/components/Flash/index.js
Normal 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;
|
36
src/components/Flash/redux/index.js
Normal file
36
src/components/Flash/redux/index.js
Normal 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
|
||||||
|
);
|
@ -7,16 +7,40 @@ import Helmet from 'react-helmet';
|
|||||||
import { StaticQuery, graphql } from 'gatsby';
|
import { StaticQuery, graphql } from 'gatsby';
|
||||||
|
|
||||||
import { fetchUser, isSignedInSelector } from '../redux';
|
import { fetchUser, isSignedInSelector } from '../redux';
|
||||||
|
import { flashMessagesSelector, removeFlashMessage } from './Flash/redux';
|
||||||
|
import Flash from './Flash';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
|
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import './global.css';
|
import './global.css';
|
||||||
|
|
||||||
const mapStateToProps = createSelector(isSignedInSelector, isSignedIn => ({
|
const propTypes = {
|
||||||
isSignedIn
|
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 =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators({ fetchUser }, dispatch);
|
bindActionCreators({ fetchUser, removeFlashMessage }, dispatch);
|
||||||
|
|
||||||
class Layout extends Component {
|
class Layout extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -30,7 +54,13 @@ class Layout extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, disableSettings } = this.props;
|
const {
|
||||||
|
children,
|
||||||
|
disableSettings,
|
||||||
|
hasMessages,
|
||||||
|
flashMessages = [],
|
||||||
|
removeFlashMessage
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<StaticQuery
|
<StaticQuery
|
||||||
query={graphql`
|
query={graphql`
|
||||||
@ -52,7 +82,12 @@ class Layout extends Component {
|
|||||||
title={data.site.siteMetadata.title}
|
title={data.site.siteMetadata.title}
|
||||||
/>
|
/>
|
||||||
<Header disableSettings={disableSettings} />
|
<Header disableSettings={disableSettings} />
|
||||||
<div style={{ marginTop: '38px' }}>{children}</div>
|
<div style={{ marginTop: '38px' }}>
|
||||||
|
{hasMessages ? (
|
||||||
|
<Flash messages={flashMessages} onClose={removeFlashMessage} />
|
||||||
|
) : null}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -60,12 +95,7 @@ class Layout extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Layout.propTypes = {
|
Layout.propTypes = propTypes;
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
disableSettings: PropTypes.bool,
|
|
||||||
fetchUser: PropTypes.func.isRequired,
|
|
||||||
isSignedIn: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
|
@ -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
|
|
@ -1,7 +1,9 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
import { reducer as app } from './';
|
import { reducer as app } from './';
|
||||||
|
import { reducer as flash } from '../components/Flash/redux';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
app
|
app,
|
||||||
|
flash
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user