Added notifications. Closes #101

- Using react-notifications to manage redux state
- Refactored Toast component to be stateless
- Toasts can be stacked
- Cleaned up CSS
- Updated stories
This commit is contained in:
Andrey Okonetchnikov 2016-10-17 12:35:31 +02:00
parent 863d90c8ee
commit f3b448106d
9 changed files with 126 additions and 109 deletions

View File

@ -46,7 +46,6 @@
"author": "Netlify",
"license": "MIT",
"devDependencies": {
"@kadira/storybook": "^1.36.0",
"babel-core": "^6.5.1",
"babel-jest": "^15.0.0",
"babel-loader": "^6.2.2",
@ -91,6 +90,7 @@
"webpack-postcss-tools": "^1.1.1"
},
"dependencies": {
"@kadira/storybook": "^1.36.0",
"autoprefixer": "^6.3.3",
"bricks.js": "^1.7.0",
"dateformat": "^1.0.12",
@ -124,6 +124,7 @@
"react-toolbox": "^1.2.1",
"react-waypoint": "^3.1.3",
"redux": "^3.3.1",
"redux-notifications": "^2.1.1",
"redux-thunk": "^1.0.3",
"selection-position": "^1.0.0",
"semaphore": "^1.0.5",

View File

@ -1,7 +1,10 @@
import { actions as notifActions } from 'redux-notifications';
import { currentBackend } from '../backends/backend';
import { getIntegrationProvider } from '../integrations';
import { getMedia, selectIntegration } from '../reducers';
const { notifSend } = notifActions;
/*
* Contant Declarations
*/
@ -190,7 +193,9 @@ export function loadEntry(entry, collection, slug) {
export function loadEntries(collection, page = 0) {
return (dispatch, getState) => {
if (collection.get('isFetching')) { return; }
if (collection.get('isFetching')) {
return;
}
const state = getState();
const integration = selectIntegration(state, collection.get('name'), 'listEntries');
const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config);
@ -218,10 +223,24 @@ export function persistEntry(collection, entryDraft) {
const mediaProxies = entryDraft.get('mediaFiles').map(path => getMedia(state, path));
const entry = entryDraft.get('entry');
dispatch(entryPersisting(collection, entry));
backend.persistEntry(state.config, collection, entryDraft, mediaProxies.toJS()).then(
() => dispatch(entryPersisted(collection, entry)),
error => dispatch(entryPersistFail(collection, entry, error))
);
backend
.persistEntry(state.config, collection, entryDraft, mediaProxies.toJS())
.then(() => {
dispatch(notifSend({
message: 'Entry saved',
kind: 'success',
dismissAfter: 4000,
}));
dispatch(entryPersisted(collection, entry));
})
.catch((error) => {
dispatch(notifSend({
message: 'Failed to persist entry',
kind: 'danger',
dismissAfter: 4000,
}));
dispatch(entryPersistFail(collection, entry, error));
});
};
}

View File

@ -2,10 +2,13 @@
--defaultColor: #333;
--defaultColorLight: #eee;
--backgroundColor: #fff;
--shadowColor: rgba(0, 0, 0, 0.117647);
--shadowColor: rgba(0, 0, 0, .25);
--infoColor: #69c;
--successColor: #1c7;
--warningColor: #fa0;
--errorColor: #f52;
--borderRadius: 2px;
--topmostZindex: 99999;
}
.base {
@ -13,14 +16,14 @@
}
.container {
color: var(--defaultColor);
background-color: var(--backgroundColor);
color: var(--defaultColor);
}
.rounded {
border-radius: 2px;
border-radius: var(--borderRadius);
}
.depth {
box-shadow: var(--shadowColor) 0px 1px 6px, var(--shadowColor) 0px 1px 4px;
box-shadow: var(--shadowColor) 0 1px 6px;
}

View File

@ -1,40 +1,41 @@
@import "../theme.css";
@import '../theme.css';
.toast {
composes: base container rounded depth;
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
width: 350px;
padding: 20px 10px;
font-size: 0.9rem;
text-align: center;
color: var(--defaultColorLight);
overflow: hidden;
opacity: 1;
transition: opacity .3s ease-in;
:root {
--iconSize: 30px;
}
.hidden {
opacity: 0;
.root {
composes: base container rounded depth from '../theme.css';
overflow: hidden;
margin: 10px;
padding: 10px 10px 15px;
color: var(--defaultColorLight);
}
.icon {
position: absolute;
top: calc(50% - 15px);
left: 15px;
font-size: 30px;
position: relative;
top: .15em;
margin-right: .25em;
font-size: var(--iconSize);
line-height: var(--iconSize);
}
.info {
composes: root;
background-color: var(--infoColor);
}
.success {
composes: root;
background-color: var(--successColor);
}
.warning {
composes: root;
background-color: var(--warningColor);
}
.error {
.danger {
composes: root;
background-color: var(--errorColor);
}

View File

@ -2,69 +2,23 @@ import React, { PropTypes } from 'react';
import { Icon } from '../index';
import styles from './Toast.css';
export default class Toast extends React.Component {
const icons = {
info: 'info',
success: 'check',
warning: 'attention',
danger: 'alert',
};
state = {
shown: false
};
componentWillMount() {
if (this.props.show) {
this.autoHideTimeout();
this.setState({ shown: true });
}
}
componentWillReceiveProps(nextProps) {
if (nextProps !== this.props) {
if (nextProps.show) this.autoHideTimeout();
this.setState({ shown: nextProps.show });
}
}
componentWillUnmount() {
if (this.timeOut) {
clearTimeout(this.timeOut);
}
}
autoHideTimeout = () => {
clearTimeout(this.timeOut);
this.timeOut = setTimeout(() => {
this.setState({ shown: false });
}, 4000);
};
render() {
const { style, type, className, children } = this.props;
const icons = {
success: 'check',
warning: 'attention',
error: 'alert'
};
const classes = [styles.toast];
if (className) classes.push(className);
let icon = '';
if (type) {
classes.push(styles[type]);
icon = <Icon type={icons[type]} className={styles.icon} />;
}
if (!this.state.shown) {
classes.push(styles.hidden);
}
return (
<div className={classes.join(' ')} style={style}>{icon}{children}</div>
);
}
export default function Toast({ kind, message }) {
return (
<div className={styles[kind]}>
<Icon type={icons[kind]} className={styles.icon} />
{message}
</div>
);
}
Toast.propTypes = {
style: PropTypes.object,
type: PropTypes.oneOf(['success', 'warning', 'error']).isRequired,
className: PropTypes.string,
show: PropTypes.bool,
children: PropTypes.node
kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired,
message: PropTypes.string,
};

View File

@ -1,19 +1,41 @@
import React from 'react';
import { Toast } from '../UI';
import { storiesOf } from '@kadira/storybook';
import { Toast } from '../UI';
const containerStyle = {
position: 'fixed',
top: 0,
right: 0,
width: 360,
height: '100%',
};
storiesOf('Toast', module)
.add('All kinds stacked', () => (
<div style={containerStyle}>
<Toast kind="info" message="A Toast Message" />
<Toast kind="success" message="A Toast Message" />
<Toast kind="warning" message="A Toast Message" />
<Toast kind="danger" message="A Toast Message" />
</div>
))
.add('Info', () => (
<div style={containerStyle}>
<Toast kind="info" message="A Toast Message" />
</div>
))
.add('Success', () => (
<div>
<Toast type='success' show>A Toast Message</Toast>
<div style={containerStyle}>
<Toast kind="success" message="A Toast Message" />
</div>
)).add('Waring', () => (
<div>
<Toast type='warning' show>A Toast Message</Toast>
))
.add('Waring', () => (
<div style={containerStyle}>
<Toast kind="warning" message="A Toast Message" />
</div>
)).add('Error', () => (
<div>
<Toast type='error' show>A Toast Message</Toast>
))
.add('Error', () => (
<div style={containerStyle}>
<Toast kind="danger" message="A Toast Message" />
</div>
));

View File

@ -1,20 +1,30 @@
@import '../components/UI/theme.css';
.layout .navDrawer .drawerContent {
padding-top: 54px;
max-width: 240px;
}
.nav {
display: block;
padding: 1rem;
& .heading {
border: none;
}
}
.main {
padding-top: 54px;
}
.navDrawer {
max-width: 240px !important;
& .drawerContent {
max-width: 240px !important;
}
.notifsContainer {
position: fixed;
top: 60px;
right: 0;
bottom: 60px;
z-index: var(--topmostZindex);
width: 360px;
pointer-events: none;
}

View File

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { Layout, Panel, NavDrawer } from 'react-toolbox/lib/layout';
import { Navigation } from 'react-toolbox/lib/navigation';
import { Link } from 'react-toolbox/lib/link';
import { Notifs } from 'redux-notifications';
import { loadConfig } from '../actions/config';
import { loginUser } from '../actions/auth';
import { currentBackend } from '../backends/backend';
@ -17,7 +18,7 @@ import {
createNewEntryInCollection,
} from '../actions/findbar';
import AppHeader from '../components/AppHeader/AppHeader';
import { Loader } from '../components/UI/index';
import { Loader, Toast } from '../components/UI/index';
import styles from './App.css';
class App extends React.Component {
@ -147,6 +148,10 @@ class App extends React.Component {
return (
<Layout theme={styles}>
<Notifs
className={styles.notifsContainer}
CustomComponent={Toast}
/>
<NavDrawer
active={navDrawerIsVisible}
scrollY

View File

@ -1,8 +1,10 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { reducer as notifReducer } from 'redux-notifications';
import reducers from '.';
export default combineReducers({
...reducers,
routing: routerReducer
notifs: notifReducer,
routing: routerReducer,
});