diff --git a/frontend/src/components/toast/index.js b/frontend/src/components/toast/index.js
new file mode 100644
index 0000000000..128b9f9d08
--- /dev/null
+++ b/frontend/src/components/toast/index.js
@@ -0,0 +1,3 @@
+import Toast from './toast'
+
+export default Toast;
diff --git a/frontend/src/components/toast/notice-container.js b/frontend/src/components/toast/notice-container.js
new file mode 100644
index 0000000000..9ed79db50f
--- /dev/null
+++ b/frontend/src/components/toast/notice-container.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { CSSTransition, TransitionGroup } from 'react-transition-group';
+import Notice from './notice';
+
+class NoticeContainer extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.transitionTime = 300;
+ this.state = {notices: []}
+ }
+
+ addNotice = (notice) => {
+ const { notices } = this.state;
+ notice.key = this.getNoticeKey();
+ notices.push(notice);
+ this.setState({notices});
+
+ if (notice.duration > 0) {
+ setTimeout(() => {
+ this.removeNotice(notice.key);
+ }, notice.duration);
+ }
+ }
+
+ removeNotice = (key) => {
+ const { notices } = this.state;
+ this.setState({
+ notices: notices.filter((notice) => {
+ if (notice.key === key) {
+ if (notice.close) {
+ setTimeout(notice.close, this.transitionTime);
+ return true;
+ }
+ return false;
+ }
+ })
+ });
+ }
+
+ getNoticeKey = () => {
+ const { notices } = this.state;
+ return `notice-${new Date().getTime()}-${notices.length}`;
+ }
+
+ render() {
+ const { notices } = this.state;
+ return (
+
+ {notices.map((notice) => {
+ return (
+
+
+
+ );
+ })}
+
+ );
+ }
+
+}
+
+export default NoticeContainer;
diff --git a/frontend/src/components/toast/notice.js b/frontend/src/components/toast/notice.js
new file mode 100644
index 0000000000..453a924bc4
--- /dev/null
+++ b/frontend/src/components/toast/notice.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ type: PropTypes.string.isRequired,
+ content: PropTypes.string.isRequired,
+};
+
+class Notice extends React.Component {
+ render() {
+ let { type, content } = this.props;
+ return (
+
+ {content}
+
+ );
+ }
+}
+
+Notice.propTypes = propTypes;
+
+export default Notice;
diff --git a/frontend/src/components/toast/toast.css b/frontend/src/components/toast/toast.css
new file mode 100644
index 0000000000..fdc97dc34c
--- /dev/null
+++ b/frontend/src/components/toast/toast.css
@@ -0,0 +1,49 @@
+.toast-notification {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.toast-notice-wrapper.notice-enter {
+ top: 0px;
+ opacity: 1;
+}
+
+.toast-notice-wrapper.notice-enter-active {
+ opacity: 1;
+ transform: translateY(0.9125rem);
+ transition: all 300ms ease-out;
+}
+
+.toast-notice-wrapper.notice-exit {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.toast-notice-wrapper.notice-exit-active {
+ opacity: 1;
+ transform: translateY(-100%);
+ transition: all 300ms ease-out;
+}
+
+.toast-notice {
+ position: relative;
+ top: 0.9125rem;
+ margin:0 auto 0.9125rem;
+ display: flex;
+ align-items: center;
+ background: #FFFFFF;
+ box-shadow: 0px 10px 20px 0px rgba(0, 0, 0, .1);
+ border-radius: 6px;
+ color: #454545;
+ font-size: 0.9125rem;
+}
+
+.toast-notice>span {
+ margin: 0;
+ padding: 0.3125rem;
+ line-height: 100%;
+}
diff --git a/frontend/src/components/toast/toast.js b/frontend/src/components/toast/toast.js
new file mode 100644
index 0000000000..ddb4d4b6d5
--- /dev/null
+++ b/frontend/src/components/toast/toast.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import NoticeContainer from './notice-container';
+import './toast.css';
+
+function createNotieContainer() {
+ const div = document.createElement('div')
+ document.body.appendChild(div)
+ const noticeContainer = ReactDOM.render(, div)
+ return {
+ addNotice(notice) {
+ return noticeContainer.addNotice(notice)
+ },
+ destroy() {
+ ReactDOM.unmountComponentAtNode(div)
+ document.body.removeChild(div)
+ }
+ }
+}
+
+let noticeContainer
+const notice = (type, content, duration = 2000, onClose) => {
+ if (!noticeContainer) noticeContainer = createNotieContainer()
+ return noticeContainer.addNotice({ type, content, duration, onClose })
+}
+
+export default {
+ info(content, duration, onClose) {
+ return notice('info', content, duration, onClose)
+ },
+ success(content, duration, onClose) {
+ return notice('success', content, duration, onClose)
+ },
+ warning(content, duration, onClose) {
+ return notice('warning', content, duration, onClose)
+ },
+ error(content, duration, onClose) {
+ return notice('danger', content, duration, onClose)
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/drafts/drafts-view.js b/frontend/src/pages/drafts/drafts-view.js
index 136630799c..e99baabf35 100644
--- a/frontend/src/pages/drafts/drafts-view.js
+++ b/frontend/src/pages/drafts/drafts-view.js
@@ -1,6 +1,7 @@
import React from 'react';
import { gettext } from '../../components/constants';
import editUtilties from '../../utils/editor-utilties';
+import Toast from '../../components/toast/';
import Loading from '../../components/loading';
import DraftListView from '../../components/draft-list-view/draft-list-view';
import DraftListMenu from '../../components/draft-list-view/draft-list-menu';
@@ -42,6 +43,9 @@ class DraftsView extends React.Component {
let draft = this.state.currentDraft;
editUtilties.deleteDraft(draft.id).then(res => {
this.initDraftList();
+ Toast.success(gettext('Delete draft succeeded.'));
+ }).catch(() => {
+ Toast.error(gettext('Delete draft failed.'));
});
}
@@ -49,6 +53,9 @@ class DraftsView extends React.Component {
let draft = this.state.currentDraft;
editUtilties.publishDraft(draft.id).then(res => {
this.initDraftList();
+ Toast.success(gettext('Publish draft succeeded.'));
+ }).catch(() => {
+ Toast.error(gettext('Publish draft failed.'));
});
}