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.')); }); }