Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #519 from LiskHQ/486-toaster
Browse files Browse the repository at this point in the history
Setup React toaster - Closes #486
  • Loading branch information
slaweet authored Aug 2, 2017
2 parents f696d2a + f0cfaaa commit b5f6739
Show file tree
Hide file tree
Showing 17 changed files with 335 additions and 7 deletions.
1 change: 1 addition & 0 deletions .storybook/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ function loadStories() {
require('../src/components/account/stories');
require('../src/components/dialog/stories');
require('../src/components/formattedNumber/stories');
require('../src/components/toaster/stories');
require('../src/components/send/stories');
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"react-circular-progressbar": "=0.1.5",
"react-dom": "=15.6.x",
"react-redux": "=5.0.3",
"react-redux-toastr": "=7.0.0",
"react-router-dom": "=4.0.0",
"react-toolbox": "=2.0.0-beta.12",
"redux": "=3.6.0",
Expand Down Expand Up @@ -89,6 +88,7 @@
"karma-verbose-reporter": "=0.0.6",
"karma-webpack": "=2.0.3",
"mocha": "=3.2.0",
"postcss-for": "=2.1.1",
"postcss-loader": "=2.0.6",
"postcss-partial-import": "=4.1.0",
"postcss-reporter": "=4.0.0",
Expand Down
34 changes: 34 additions & 0 deletions src/actions/toaster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import actionTypes from '../constants/actions';

/**
* An action to dispatch to display a toast
*
*/
export const toastDisplayed = data => ({
data,
type: actionTypes.toastDisplayed,
});

/**
* An action to dispatch to display a success toast
*
*/
export const successToastDisplayed = ({ type = 'success', ...rest }) =>
toastDisplayed({ type, ...rest });


/**
* An action to dispatch to display an error toast
*
*/
export const errorToastDisplayed = ({ type = 'error', ...rest }) =>
toastDisplayed({ type, ...rest });

/**
* An action to dispatch to hide a toast
*
*/
export const toastHidden = data => ({
data,
type: actionTypes.toastHidden,
});
51 changes: 51 additions & 0 deletions src/actions/toaster.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect } from 'chai';
import actionTypes from '../constants/actions';
import { toastDisplayed, successToastDisplayed, errorToastDisplayed, toastHidden } from './toaster';

describe('actions: toaster', () => {
const data = {
label: 'dummy',
};

describe('toastDisplayed', () => {
it('should create an action to show toast', () => {


const expectedAction = {
data,
type: actionTypes.toastDisplayed,
};
expect(toastDisplayed(data)).to.be.deep.equal(expectedAction);
});
});

describe('successToastDisplayed', () => {
it('should create an action to show success toast', () => {
const expectedAction = {
data: { ...data, type: 'success' },
type: actionTypes.toastDisplayed,
};
expect(successToastDisplayed(data)).to.be.deep.equal(expectedAction);
});
});

describe('errorToastDisplayed', () => {
it('should create an action to show error toast', () => {
const expectedAction = {
data: { ...data, type: 'error' },
type: actionTypes.toastDisplayed,
};
expect(errorToastDisplayed(data)).to.be.deep.equal(expectedAction);
});
});

describe('toastHidden', () => {
it('should create an action to hide toast', () => {
const expectedAction = {
data,
type: actionTypes.toastHidden,
};
expect(toastHidden(data)).to.be.deep.equal(expectedAction);
});
});
});
2 changes: 2 additions & 0 deletions src/components/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Forging from '../forging';
import styles from './app.css';
import Metronome from '../../utils/metronome';
import Dialog from '../dialog';
import Toaster from '../toaster';
import Tabs from '../tabs';
import LoadingBar from '../loadingBar';

Expand All @@ -33,6 +34,7 @@ const App = () => (
<Route exact path="/" component={Login} />
</main>
<Dialog />
<Toaster />
<LoadingBar />
</section>
);
Expand Down
19 changes: 13 additions & 6 deletions src/components/signVerify/signMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import React from 'react';
import Input from 'react-toolbox/lib/input';
import Button from 'react-toolbox/lib/button';
import copy from 'copy-to-clipboard';
import { toastr } from 'react-redux-toastr';
import { connect } from 'react-redux';
import grid from 'flexboxgrid/dist/flexboxgrid.css';

import lisk from 'lisk-js';

import { successToastDisplayed } from '../../actions/toaster';
import InfoParagraph from '../infoParagraph';
import SignVerifyResult from './signVerifyResult';

Expand Down Expand Up @@ -38,17 +40,15 @@ class SignMessage extends React.Component {
message: 'Press #{key} to copy',
});
if (copied) {
// TODO: set up the toaster in redux
// https://github.com/diegoddox/react-redux-toastr
toastr.success('Result copied to clipboard');
this.props.successToast({ label: 'Result copied to clipboard' });
}
this.setState({ resultIsShown: true });
}
}

render() {
return (
<div className='verify-message'>
<div className='sign-message'>
<InfoParagraph>
Signing a message with this tool indicates ownership of a privateKey (secret) and
provides a level of proof that you are the owner of the key.
Expand Down Expand Up @@ -80,4 +80,11 @@ class SignMessage extends React.Component {
}
}

export default SignMessage;
const mapDispatchToProps = dispatch => ({
successToast: data => dispatch(successToastDisplayed(data)),
});

export default connect(
null,
mapDispatchToProps,
)(SignMessage);
18 changes: 18 additions & 0 deletions src/components/toaster/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import ToasterComponent from './toasterComponent';
import { toastHidden } from '../../actions/toaster';

const mapStateToProps = state => ({
toasts: state.toaster || [],
});

const mapDispatchToProps = dispatch => ({
hideToast: data => dispatch(toastHidden(data)),
});

const Toaster = connect(
mapStateToProps,
mapDispatchToProps,
)(ToasterComponent);

export default Toaster;
22 changes: 22 additions & 0 deletions src/components/toaster/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import Toaster from './';
import store from '../../store';

chai.use(sinonChai);


describe('Toaster', () => {
let wrapper;

beforeEach(() => {
wrapper = mount(<Provider store={store}><Toaster /></Provider>);
});

it('should render ToasterComponent', () => {
expect(wrapper.find('ToasterComponent')).to.have.lengthOf(1);
});
});
28 changes: 28 additions & 0 deletions src/components/toaster/stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';

import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Toaster from './toasterComponent';


storiesOf('Toaster', module)
.add('default', () => (
<Toaster
label='Test toast'
hideToast={ action('onHide') }
/>
))
.add('success', () => (
<Toaster
label='Success toast'
type='success'
hideToast={ action('onHide') }
/>
))
.add('error', () => (
<Toaster
label='Error toast'
type='error'
hideToast={ action('onHide') }
/>
));
28 changes: 28 additions & 0 deletions src/components/toaster/toaster.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.toast {
color: white;
left: initial;
}

@for $i from 0 to 10 {
@keyframes move-$i {
from {
bottom: -50px;
}
to {
bottom: calc($(i) * 50px + 10px);
}
}
.index-$i {
animation: move-$i 0.5s ease-in;
bottom: calc($(i) * 50px + 10px);
}
}


.error {
background-color: #c62828;
}

.success {
background-color: #7cb342;
}
37 changes: 37 additions & 0 deletions src/components/toaster/toasterComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { Component } from 'react';
import { Snackbar } from 'react-toolbox';
import styles from './toaster.css';

class ToasterComponent extends Component {
constructor() {
super();
this.state = {
hidden: {},
};
}

hideToast(toast) {
setTimeout(() => {
this.props.hideToast(toast);
this.setState({ hidden: { ...this.state.hidden, [toast.index]: false } });
}, 500);
this.setState({ hidden: { ...this.state.hidden, [toast.index]: true } });
}

render() {
return (<span>
{this.props.toasts.map(toast => (
<Snackbar
active={!!toast.label && !this.state.hidden[toast.index]}
key={toast.index}
label={toast.label}
timeout={4000}
className={`${styles.toast} ${styles[toast.type]} ${styles[`index-${toast.index}`]}`}
onTimeout={this.hideToast.bind(this, toast)}
/>
))}
</span>);
}
}

export default ToasterComponent;
42 changes: 42 additions & 0 deletions src/components/toaster/toasterComponent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import chai, { expect } from 'chai';
import sinon from 'sinon';
import { mount } from 'enzyme';
import chaiEnzyme from 'chai-enzyme';
import sinonChai from 'sinon-chai';
import ToasterComponent from './toasterComponent';

chai.use(sinonChai);
chai.use(chaiEnzyme()); // Note the invocation at the end
describe('ToasterComponent', () => {
let wrapper;
const toasts = [{
label: 'test',
type: 'success',
index: 0,
}];
const toasterProps = {
toasts,
hideToast: sinon.spy(),
};

beforeEach(() => {
wrapper = mount(<ToasterComponent {...toasterProps} />);
});

it('renders <Snackbar /> component from react-toolbox', () => {
expect(wrapper.find('Snackbar')).to.have.length(1);
});

describe('hideToast', () => {
it('hides the toast and after the animation ends calls this.props.hideToast()', () => {
const clock = sinon.useFakeTimers();
wrapper.instance().hideToast(toasts[0]);
expect(wrapper.state('hidden')).to.deep.equal({ [toasts[0].index]: true });
clock.tick(510);
expect(wrapper.state('hidden')).to.deep.equal({ [toasts[0].index]: false });
clock.restore();
expect(toasterProps.hideToast).to.have.been.calledWith(toasts[0]);
});
});
});
2 changes: 2 additions & 0 deletions src/constants/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const actionTypes = {
dialogHidden: 'DIALOG_HIDDEN',
forgedBlocksUpdated: 'FORGED_BLOCKS_UPDATED',
forgingStatsUpdated: 'FORGING_STATS_UPDATED',
toastDisplayed: 'TOAST_DISPLAYED',
toastHidden: 'TOAST_HIDDEN',
loadingStarted: 'LOADING_STARTED',
loadingFinished: 'LOADING_FINISHED',
};
Expand Down
1 change: 1 addition & 0 deletions src/store/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { default as peers } from './peers';
export { default as dialog } from './dialog';
export { default as forging } from './forging';
export { default as loading } from './loading';
export { default as toaster } from './toaster';

25 changes: 25 additions & 0 deletions src/store/reducers/toaster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import actionTypes from '../../constants/actions';

/**
*
* @param {Array} state
* @param {Object} action
*/
const toaster = (state = [], action) => {
switch (action.type) {
case actionTypes.toastDisplayed:
return [
...state,
{
...action.data,
index: state.length ? state[state.length - 1].index + 1 : 0,
},
];
case actionTypes.toastHidden:
return state.filter(toast => toast.index !== action.data.index);
default:
return state;
}
};

export default toaster;
Loading

0 comments on commit b5f6739

Please sign in to comment.