Feature(toast): Move from react-toastr to react-notifications
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Button, Row } from 'react-bootstrap';
|
||||
import { ToastMessage, ToastContainer } from 'react-toastr';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
@@ -16,11 +15,9 @@ import {
|
||||
import { submitChallenge } from './routes/challenges/redux/actions';
|
||||
|
||||
import Nav from './components/Nav';
|
||||
import { randomCompliment } from './utils/get-words';
|
||||
import Toasts from './toasts/Toasts.jsx';
|
||||
import { userSelector } from './redux/selectors';
|
||||
|
||||
const toastMessageFactory = React.createFactory(ToastMessage.animation);
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
state => state.app.shouldShowSignIn,
|
||||
@@ -34,7 +31,6 @@ const mapStateToProps = createSelector(
|
||||
toast,
|
||||
isMapDrawerOpen,
|
||||
isMapAlreadyLoaded,
|
||||
showChallengeComplete
|
||||
) => ({
|
||||
username,
|
||||
points,
|
||||
@@ -43,7 +39,6 @@ const mapStateToProps = createSelector(
|
||||
shouldShowSignIn,
|
||||
isMapDrawerOpen,
|
||||
isMapAlreadyLoaded,
|
||||
showChallengeComplete,
|
||||
isSignedIn: !!username
|
||||
})
|
||||
);
|
||||
@@ -72,7 +67,6 @@ export class FreeCodeCamp extends React.Component {
|
||||
toast: PropTypes.object,
|
||||
updateNavHeight: PropTypes.func,
|
||||
initWindowHeight: PropTypes.func,
|
||||
showChallengeComplete: PropTypes.number,
|
||||
submitChallenge: PropTypes.func,
|
||||
isMapDrawerOpen: PropTypes.bool,
|
||||
isMapAlreadyLoaded: PropTypes.bool,
|
||||
@@ -83,38 +77,6 @@ export class FreeCodeCamp extends React.Component {
|
||||
params: PropTypes.object
|
||||
};
|
||||
|
||||
componentWillReceiveProps({
|
||||
toast: nextToast = {},
|
||||
showChallengeComplete: nextCC = 0
|
||||
}) {
|
||||
const {
|
||||
toast = {},
|
||||
showChallengeComplete
|
||||
} = this.props;
|
||||
if (toast.id !== nextToast.id) {
|
||||
this.refs.toaster[nextToast.type || 'success'](
|
||||
nextToast.message,
|
||||
nextToast.title,
|
||||
{
|
||||
closeButton: true,
|
||||
timeOut: 10000
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (nextCC !== showChallengeComplete) {
|
||||
this.refs.toaster.success(
|
||||
this.renderChallengeComplete(),
|
||||
randomCompliment(),
|
||||
{
|
||||
closeButton: true,
|
||||
timeOut: 0,
|
||||
extendedTimeOut: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.initWindowHeight();
|
||||
if (!this.props.isSignedIn) {
|
||||
@@ -173,11 +135,7 @@ export class FreeCodeCamp extends React.Component {
|
||||
isOpen={ isMapDrawerOpen }
|
||||
toggleMapDrawer={ toggleMapDrawer }
|
||||
/>
|
||||
<ToastContainer
|
||||
className='toast-bottom-right'
|
||||
ref='toaster'
|
||||
toastMessageFactory={ toastMessageFactory }
|
||||
/>
|
||||
<Toasts />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import { reducer as app } from './redux';
|
||||
import { reducer as toasts } from './toasts/redux';
|
||||
import entitiesReducer from './redux/entities-reducer';
|
||||
import {
|
||||
reducer as challengesApp,
|
||||
@@ -13,6 +14,7 @@ export default function createReducer(sideReducers = {}) {
|
||||
...sideReducers,
|
||||
entities: entitiesReducer,
|
||||
app,
|
||||
toasts,
|
||||
challengesApp,
|
||||
form: formReducer.normalize({ ...projectNormalizer })
|
||||
});
|
||||
|
@@ -19,11 +19,6 @@ export default handleActions(
|
||||
title: payload + ' | Free Code Camp'
|
||||
}),
|
||||
|
||||
[types.makeToast]: (state, { payload: toast }) => ({
|
||||
...state,
|
||||
toast
|
||||
}),
|
||||
|
||||
[types.updateThisUser]: (state, { payload: user }) => ({
|
||||
...state,
|
||||
user,
|
||||
|
@@ -10,7 +10,6 @@ export default createTypes([
|
||||
'updateCompletedChallenges',
|
||||
'showSignIn',
|
||||
|
||||
'makeToast',
|
||||
'handleError',
|
||||
'toggleNightMode',
|
||||
// used to hit the server
|
||||
|
@@ -11,7 +11,7 @@ import Output from './Output.jsx';
|
||||
import ToolPanel from './Tool-Panel.jsx';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import { updateHint, executeChallenge } from '../../redux/actions';
|
||||
import { makeToast } from '../../../../redux/actions';
|
||||
import { makeToast } from '../../../../toasts/redux/actions';
|
||||
|
||||
const bindableActions = { makeToast, executeChallenge, updateHint };
|
||||
const mapStateToProps = createSelector(
|
||||
|
@@ -93,14 +93,6 @@ export const updateOutput = createAction(types.updateOutput, loggerToStr);
|
||||
export const checkChallenge = createAction(types.checkChallenge);
|
||||
|
||||
export const showProjectSubmit = createAction(types.showProjectSubmit);
|
||||
let id = 0;
|
||||
export const showChallengeComplete = createAction(
|
||||
types.showChallengeComplete,
|
||||
() => {
|
||||
id += 1;
|
||||
return id;
|
||||
}
|
||||
);
|
||||
|
||||
export const submitChallenge = createAction(types.submitChallenge);
|
||||
export const moveToNextChallenge = createAction(types.moveToNextChallenge);
|
||||
|
@@ -3,7 +3,8 @@ import types from './types';
|
||||
import { getMouse } from '../utils';
|
||||
|
||||
import { submitChallenge, videoCompleted } from './actions';
|
||||
import { createErrorObservable, makeToast } from '../../../redux/actions';
|
||||
import { createErrorObservable } from '../../../redux/actions';
|
||||
import { makeToast } from '../../../toasts/redux/actions';
|
||||
import { challengeSelector } from './selectors';
|
||||
|
||||
export default function answerSaga(action$, getState) {
|
||||
@@ -54,11 +55,7 @@ export default function answerSaga(action$, getState) {
|
||||
if (answer !== finalAnswer) {
|
||||
let infoAction;
|
||||
if (info) {
|
||||
infoAction = makeToast({
|
||||
title: 'Have a hint',
|
||||
message: info,
|
||||
type: 'info'
|
||||
});
|
||||
infoAction = makeToast({ message: info });
|
||||
}
|
||||
|
||||
return Observable
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import types from './types';
|
||||
import { showChallengeComplete, moveToNextChallenge } from './actions';
|
||||
import { moveToNextChallenge } from './actions';
|
||||
import {
|
||||
createErrorObservable,
|
||||
makeToast,
|
||||
updateUserPoints
|
||||
} from '../../../redux/actions';
|
||||
import { makeToast } from '../../../toasts/redux/actions';
|
||||
|
||||
import { challengeSelector } from './selectors';
|
||||
import { backEndProject } from '../../../utils/challengeTypes';
|
||||
@@ -18,9 +19,8 @@ function postChallenge(url, body, username) {
|
||||
.flatMap(({ alreadyCompleted, points }) => {
|
||||
return Observable.of(
|
||||
makeToast({
|
||||
message: randomCompliment() +
|
||||
(alreadyCompleted ? '!' : '! First time Completed!'),
|
||||
type: 'info'
|
||||
message: randomCompliment() +
|
||||
(alreadyCompleted ? '!' : '! First time Completed!')
|
||||
}),
|
||||
updateUserPoints(username, points)
|
||||
);
|
||||
@@ -29,7 +29,7 @@ function postChallenge(url, body, username) {
|
||||
|
||||
const challengeCompleted$ = Observable.of(
|
||||
moveToNextChallenge(),
|
||||
username ? makeToast({ message: ' Saving...', type: 'info' }) : null
|
||||
username ? makeToast({ message: ' Saving...' }) : null
|
||||
);
|
||||
return Observable.merge(saveChallenge$, challengeCompleted$);
|
||||
}
|
||||
@@ -39,7 +39,11 @@ function submitModern(type, state) {
|
||||
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
|
||||
if (type === types.checkChallenge) {
|
||||
return Observable.of(
|
||||
showChallengeComplete()
|
||||
makeToast({
|
||||
message: 'Go to my next challenge.',
|
||||
action: 'Submit',
|
||||
actionCreator: 'submitChallenge'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,9 +62,7 @@ function submitModern(type, state) {
|
||||
}
|
||||
}
|
||||
return Observable.just(makeToast({
|
||||
message: 'Not all tests are passing, yet.',
|
||||
title: 'Almost There!',
|
||||
type: 'info'
|
||||
message: 'Not all tests are passing, yet.'
|
||||
}));
|
||||
}
|
||||
|
||||
|
@@ -2,13 +2,13 @@ import { Observable } from 'rx';
|
||||
import { push } from 'react-router-redux';
|
||||
import types from './types';
|
||||
import { resetUi, updateCurrentChallenge } from './actions';
|
||||
import { createErrorObservable, makeToast } from '../../../redux/actions';
|
||||
import { createErrorObservable } from '../../../redux/actions';
|
||||
import { makeToast } from '../../../toasts/redux/actions';
|
||||
import {
|
||||
getNextChallenge,
|
||||
getFirstChallengeOfNextBlock,
|
||||
getFirstChallengeOfNextSuperBlock
|
||||
} from '../utils';
|
||||
import { randomVerb } from '../../../utils/get-words';
|
||||
import debug from 'debug';
|
||||
|
||||
const isDev = debug.enabled('fcc:*');
|
||||
@@ -64,10 +64,7 @@ export default function nextChallengeSaga(actions$, getState) {
|
||||
return Observable.of(
|
||||
updateCurrentChallenge(nextChallenge),
|
||||
resetUi(),
|
||||
makeToast({
|
||||
title: randomVerb(),
|
||||
message: 'Your next challenge has arrived.'
|
||||
}),
|
||||
makeToast({ message: 'Your next challenge arrived.' }),
|
||||
push(`/challenges/${nextChallenge.block}/${nextChallenge.dashedName}`)
|
||||
);
|
||||
} catch (err) {
|
||||
|
69
common/app/toasts/Toasts.jsx
Normal file
69
common/app/toasts/Toasts.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { NotificationStack } from 'react-notification';
|
||||
|
||||
import { removeToast } from './redux/actions';
|
||||
import { submitChallenge } from '../routes/challenges/redux/actions';
|
||||
|
||||
const registeredActions = { submitChallenge };
|
||||
const mapStateToProps = state => ({ toasts: state.toasts });
|
||||
const barStyle = {
|
||||
fontSize: '2rem',
|
||||
// null values let our css set the style prop
|
||||
padding: null
|
||||
};
|
||||
const actionStyle = {
|
||||
fontSize: '2rem'
|
||||
};
|
||||
const addDispatchableActionsToToast = createSelector(
|
||||
state => state.toasts,
|
||||
state => state.dispatch,
|
||||
(toasts, dispatch) => toasts.map(({ position, actionCreator, ...toast }) => {
|
||||
const activeBarStyle = {};
|
||||
if (position !== 'left') {
|
||||
activeBarStyle.left = null;
|
||||
activeBarStyle.right = '1rem';
|
||||
}
|
||||
const onClick = registeredActions[actionCreator] ?
|
||||
() => dispatch(registeredActions[actionCreator]()) :
|
||||
null;
|
||||
return {
|
||||
...toast,
|
||||
barStyle,
|
||||
activeBarStyle,
|
||||
actionStyle,
|
||||
onClick
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
export class Toasts extends React.Component {
|
||||
constructor(...props) {
|
||||
super(...props);
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
}
|
||||
static displayName = 'Toasts';
|
||||
static propTypes = {
|
||||
toasts: PropTypes.arrayOf(PropTypes.object),
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
handleDismiss(notification) {
|
||||
this.props.dispatch(removeToast(notification));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { toasts = [], dispatch } = this.props;
|
||||
return (
|
||||
<NotificationStack
|
||||
notifications={
|
||||
addDispatchableActionsToToast({ toasts, dispatch })
|
||||
}
|
||||
onDismiss={ this.handleDismiss }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Toasts);
|
20
common/app/toasts/redux/actions.js
Normal file
20
common/app/toasts/redux/actions.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import types from './types';
|
||||
|
||||
let key = 0;
|
||||
export const makeToast = createAction(
|
||||
types.makeToast,
|
||||
({ timeout, ...rest }) => ({
|
||||
...rest,
|
||||
// assign current value of key to new toast
|
||||
// and then increment key value
|
||||
key: key++,
|
||||
dismissAfter: timeout || 40000,
|
||||
position: rest.position === 'left' ? 'left' : 'right'
|
||||
})
|
||||
);
|
||||
|
||||
export const removeToast = createAction(
|
||||
types.removeToast,
|
||||
({ key }) => key
|
||||
);
|
3
common/app/toasts/redux/index.js
Normal file
3
common/app/toasts/redux/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as types } from './types';
|
||||
export { default as reducer } from './reducer';
|
||||
export * as actions from './actions';
|
13
common/app/toasts/redux/reducer.js
Normal file
13
common/app/toasts/redux/reducer.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
import types from './types';
|
||||
|
||||
const initialState = [];
|
||||
export default handleActions({
|
||||
[types.makeToast]: (state, { payload: toast }) => [
|
||||
...state,
|
||||
toast
|
||||
],
|
||||
[types.removeToast]: (state, { payload: key }) => state.filter(
|
||||
toast => toast.key !== key
|
||||
)
|
||||
}, initialState);
|
6
common/app/toasts/redux/types.js
Normal file
6
common/app/toasts/redux/types.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import createTypes from '../../utils/create-types';
|
||||
|
||||
export default createTypes([
|
||||
'makeToast',
|
||||
'removeToast'
|
||||
], 'toast');
|
Reference in New Issue
Block a user