diff --git a/client/less/toastr.less b/client/less/toastr.less index f4774004a2..de17335ed3 100644 --- a/client/less/toastr.less +++ b/client/less/toastr.less @@ -1,269 +1,10 @@ -// sourced from https://github.com/CodeSeven/toastr -// MIT license -// Mix-ins -.borderRadius(@radius) { - -moz-border-radius: @radius; - -webkit-border-radius: @radius; - border-radius: @radius; +.notification-bar { + z-index: 999999; + overflow: hidden; + // margin: 0 0 6px; + padding: 2rem; } -.boxShadow(@boxShadow) { - -moz-box-shadow: @boxShadow; - -webkit-box-shadow: @boxShadow; - box-shadow: @boxShadow; -} - -.opacity(@opacity) { - @opacityPercent: @opacity * 100; - opacity: @opacity; - -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})"; - filter: ~"alpha(opacity=@{opacityPercent})"; -} - -.wordWrap(@wordWrap: break-word) { - -ms-word-wrap: @wordWrap; - word-wrap: @wordWrap; -} - -// Variables -@black: #000000; -@grey: #999999; -@light-grey: #CCCCCC; -@white: #FFFFFF; -@near-black: #030303; -@green: #51A351; -@red: #BD362F; -@blue: #2F96B4; -@orange: #F89406; -@default-container-opacity: .8; - -// Styles -.toast-title { - font-weight: bold; -} - -.toast-message { - .wordWrap(); - - a, - label { - color: @white; - } - - a:hover { - color: @light-grey; - text-decoration: none; - } -} - -.toast-close-button { - position: relative; - right: -0.3em; - top: -0.3em; - float: right; - font-size: 20px; - font-weight: bold; - color: @white; - -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1); - text-shadow: 0 1px 0 rgba(255,255,255,1); - .opacity(0.8); - - &:hover, - &:focus { - color: @black; - text-decoration: none; - cursor: pointer; - .opacity(0.4); - } -} - -/*Additional properties for button version - iOS requires the button element instead of an anchor tag. - If you want the anchor version, it requires `href="#"`.*/ -button.toast-close-button { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} - -//#endregion - -.toast-top-center { - top: 0; - right: 0; - width: 100%; -} - -.toast-bottom-center { - bottom: 0; - right: 0; - width: 100%; -} - -.toast-top-full-width { - top: 0; - right: 0; - width: 100%; -} - -.toast-bottom-full-width { - bottom: 0; - right: 0; - width: 100%; -} - -.toast-top-left { - top: 12px; - left: 12px; -} - -.toast-top-right { - top: 12px; - right: 12px; -} - -.toast-bottom-right { - right: 12px; - bottom: 12px; -} - -.toast-bottom-left { - bottom: 12px; - left: 12px; -} - -#toast-container { - position: fixed; - z-index: 999999; - // The container should not be clickable. - pointer-events: none; - * { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - } - - > div { - position: relative; - // The toast itself should be clickable. - pointer-events: auto; - overflow: hidden; - margin: 0 0 6px; - padding: 15px 15px 15px 50px; - width: 300px; - .borderRadius(3px 3px 3px 3px); - background-position: 15px center; - background-repeat: no-repeat; - .boxShadow(0 0 12px @grey); - color: @white; - .opacity(@default-container-opacity); - } - - > :hover { - .boxShadow(0 0 12px @black); - .opacity(1); - cursor: pointer; - } - - > .toast-info { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; - } - - > .toast-error { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; - } - - > .toast-success { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; - } - - > .toast-warning { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; - } - - /*overrides*/ - &.toast-top-center > div, - &.toast-bottom-center > div { - width: 300px; - margin-left: auto; - margin-right: auto; - } - - &.toast-top-full-width > div, - &.toast-bottom-full-width > div { - width: 96%; - margin-left: auto; - margin-right: auto; - } -} - -.toast { - background-color: @near-black; -} - -.toast-success { - background-color: @green; -} - -.toast-error { - background-color: @red; -} - -.toast-info { - background-color: @blue; -} - -.toast-warning { - background-color: @orange; -} - -.toast-progress { - position: absolute; - left: 0; - bottom: 0; - height: 4px; - background-color: @black; - .opacity(0.4); -} - -/*Responsive Design*/ - -@media all and (max-width: 240px) { - #toast-container { - - > div { - padding: 8px 8px 8px 50px; - width: 11em; - } - - & .toast-close-button { - right: -0.2em; - top: -0.2em; - } - } -} - -@media all and (min-width: 241px) and (max-width: 480px) { - #toast-container { - > div { - padding: 8px 8px 8px 50px; - width: 18em; - } - - & .toast-close-button { - right: -0.2em; - top: -0.2em; - } - } -} - -@media all and (min-width: 481px) and (max-width: 768px) { - #toast-container { - > div { - padding: 15px 15px 15px 50px; - width: 25em; - } - } +.notification-bar-message { + padding-right: 2rem; } diff --git a/common/app/App.jsx b/common/app/App.jsx index 9e336e2dd8..f8939b5654 100644 --- a/common/app/App.jsx +++ b/common/app/App.jsx @@ -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 } /> - + ); } diff --git a/common/app/create-reducer.js b/common/app/create-reducer.js index f7edc27f93..80964bc069 100644 --- a/common/app/create-reducer.js +++ b/common/app/create-reducer.js @@ -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 }) }); diff --git a/common/app/redux/reducer.js b/common/app/redux/reducer.js index c26f11cfa5..48b1a95226 100644 --- a/common/app/redux/reducer.js +++ b/common/app/redux/reducer.js @@ -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, diff --git a/common/app/redux/types.js b/common/app/redux/types.js index 46a8307270..ab0864c0c7 100644 --- a/common/app/redux/types.js +++ b/common/app/redux/types.js @@ -10,7 +10,6 @@ export default createTypes([ 'updateCompletedChallenges', 'showSignIn', - 'makeToast', 'handleError', 'toggleNightMode', // used to hit the server diff --git a/common/app/routes/challenges/components/classic/Side-Panel.jsx b/common/app/routes/challenges/components/classic/Side-Panel.jsx index a81497437b..a6b0f4f5fc 100644 --- a/common/app/routes/challenges/components/classic/Side-Panel.jsx +++ b/common/app/routes/challenges/components/classic/Side-Panel.jsx @@ -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( diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js index 8890e2251f..d4f6a158ef 100644 --- a/common/app/routes/challenges/redux/actions.js +++ b/common/app/routes/challenges/redux/actions.js @@ -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); diff --git a/common/app/routes/challenges/redux/answer-saga.js b/common/app/routes/challenges/redux/answer-saga.js index b9ccedf028..def66ecb90 100644 --- a/common/app/routes/challenges/redux/answer-saga.js +++ b/common/app/routes/challenges/redux/answer-saga.js @@ -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 diff --git a/common/app/routes/challenges/redux/completion-saga.js b/common/app/routes/challenges/redux/completion-saga.js index f347d0ea1a..e90368fdcc 100644 --- a/common/app/routes/challenges/redux/completion-saga.js +++ b/common/app/routes/challenges/redux/completion-saga.js @@ -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.' })); } diff --git a/common/app/routes/challenges/redux/next-challenge-saga.js b/common/app/routes/challenges/redux/next-challenge-saga.js index 55f04e4b57..5352149ae3 100644 --- a/common/app/routes/challenges/redux/next-challenge-saga.js +++ b/common/app/routes/challenges/redux/next-challenge-saga.js @@ -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) { diff --git a/common/app/toasts/Toasts.jsx b/common/app/toasts/Toasts.jsx new file mode 100644 index 0000000000..de3bb90a32 --- /dev/null +++ b/common/app/toasts/Toasts.jsx @@ -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 ( + + ); + } +} + +export default connect(mapStateToProps)(Toasts); diff --git a/common/app/toasts/redux/actions.js b/common/app/toasts/redux/actions.js new file mode 100644 index 0000000000..0241ba1fd1 --- /dev/null +++ b/common/app/toasts/redux/actions.js @@ -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 +); diff --git a/common/app/toasts/redux/index.js b/common/app/toasts/redux/index.js new file mode 100644 index 0000000000..3a828d6dcd --- /dev/null +++ b/common/app/toasts/redux/index.js @@ -0,0 +1,3 @@ +export { default as types } from './types'; +export { default as reducer } from './reducer'; +export * as actions from './actions'; diff --git a/common/app/toasts/redux/reducer.js b/common/app/toasts/redux/reducer.js new file mode 100644 index 0000000000..ac1312041c --- /dev/null +++ b/common/app/toasts/redux/reducer.js @@ -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); diff --git a/common/app/toasts/redux/types.js b/common/app/toasts/redux/types.js new file mode 100644 index 0000000000..5472d76070 --- /dev/null +++ b/common/app/toasts/redux/types.js @@ -0,0 +1,6 @@ +import createTypes from '../../utils/create-types'; + +export default createTypes([ + 'makeToast', + 'removeToast' +], 'toast'); diff --git a/package.json b/package.json index f8a5394de4..2a161ef7b5 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react-images": "^0.4.6", "react-motion": "~0.4.2", "react-no-ssr": "^1.0.1", + "react-notification": "^5.0.7", "react-pure-render": "^1.0.2", "react-redux": "^4.0.6", "react-router": "^2.0.0",