Skip to content

Commit 9bec6d6

Browse files
committed
Implement simple toast
1 parent 0b486e2 commit 9bec6d6

File tree

7 files changed

+191
-1
lines changed

7 files changed

+191
-1
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// @flow
2+
import React, { Component } from 'react';
3+
import cx from 'classnames';
4+
import './NotifyToast.scss';
5+
6+
type Props = {
7+
toast: {
8+
type: ?string,
9+
message: ?string,
10+
visible: boolean,
11+
},
12+
onHide: () => void,
13+
};
14+
15+
type State = {
16+
animating: boolean,
17+
};
18+
19+
class NotifyToast extends Component<Props, State> {
20+
state = {
21+
animating: false,
22+
};
23+
autoHideTimeout = null;
24+
animateTimeout = null;
25+
componentDidUpdate(prevProps: Props, prevState: State) {
26+
// false -> true
27+
if (!prevProps.toast.visible && this.props.toast.visible) {
28+
if (this.autoHideTimeout) {
29+
clearTimeout(this.autoHideTimeout);
30+
this.autoHideTimeout = null;
31+
}
32+
this.autoHideTimeout = setTimeout(() => {
33+
this.props.onHide();
34+
this.autoHideTimeout = null;
35+
}, 1500);
36+
}
37+
38+
if (prevProps.toast.visible !== this.props.toast.visible) {
39+
this.setState({
40+
animating: true,
41+
});
42+
if (this.animateTimeout) {
43+
clearTimeout(this.animateTimeout);
44+
this.animateTimeout = null;
45+
}
46+
this.animateTimeout = setTimeout(() => {
47+
this.setState({
48+
animating: false,
49+
});
50+
}, 150);
51+
}
52+
}
53+
render() {
54+
const { type, message, visible } = this.props.toast;
55+
const { animating } = this.state;
56+
const transition = (() => {
57+
if (!animating) return '';
58+
return visible ? 'appear' : 'disappear';
59+
})();
60+
if (!animating && !visible) return null;
61+
if (!type || !message) return null;
62+
return <div className={cx('NotifyToast', type, transition)}>{message}</div>;
63+
}
64+
}
65+
66+
export default NotifyToast;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@import "utils";
2+
@keyframes notifyToastAppear {
3+
0% {
4+
transform: translateY(calc(100% + 1rem));
5+
}
6+
100% {
7+
transform: translateY(0%);
8+
}
9+
}
10+
11+
@keyframes notifyToastDisappear {
12+
0% {
13+
transform: translateY(0%);
14+
}
15+
100% {
16+
transform: translateY(calc(100% + 1rem));
17+
}
18+
}
19+
20+
.NotifyToast {
21+
position: fixed;
22+
bottom: 1rem;
23+
right: 1rem;
24+
&.default {
25+
}
26+
&.success {
27+
border-radius: 1.40625rem;
28+
background: $oc-green-4;
29+
color: white;
30+
min-width: 200px;
31+
padding: 0.75rem;
32+
padding-left: 1rem;
33+
display: flex;
34+
justify-content: center;
35+
padding-right: 1rem;
36+
font-size: 0.875rem;
37+
line-height: 1.3125rem;
38+
font-weight: 600;
39+
}
40+
&.error {
41+
}
42+
&.appear {
43+
animation: notifyToastAppear 0.15s ease-in;
44+
animation-fill-mode: forwards;
45+
}
46+
&.disappear {
47+
animation: notifyToastDisappear 0.15s ease-in;
48+
animation-fill-mode: forwards;
49+
}
50+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// @flow
2+
export { default } from './NotifyToast';

velog-frontend/src/containers/base/Core.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { withRouter, type ContextRouter } from 'react-router-dom';
1111

1212
import FullscreenLoaderContainer from './FullscreenLoaderContainer';
1313
import { setup } from '../../lib/progress';
14+
import NotifyToastContainer from './NotifyToastContainer';
1415

1516
type Props = {
1617
user: ?UserData,
@@ -82,6 +83,7 @@ class Core extends Component<Props> {
8283
<Fragment>
8384
<FullscreenLoaderContainer />
8485
<NanoBar />
86+
<NotifyToastContainer />
8587
</Fragment>
8688
);
8789
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// @flow
2+
import React, { Component } from 'react';
3+
import { connect } from 'react-redux';
4+
import { BaseActions } from 'store/actionCreators';
5+
import { type State } from 'store';
6+
7+
import NotifyToast from '../../components/base/NotifyToast';
8+
9+
type Props = {
10+
toast: {
11+
type: ?string,
12+
message: ?string,
13+
visible: boolean,
14+
},
15+
};
16+
class NotifyToastContainer extends Component<Props> {
17+
onHide() {
18+
BaseActions.hideToast();
19+
}
20+
21+
render() {
22+
const { toast } = this.props;
23+
24+
return <NotifyToast toast={toast} onHide={this.onHide} />;
25+
}
26+
}
27+
28+
export default connect(
29+
({ base }: State) => ({
30+
toast: base.toast,
31+
}),
32+
() => ({}),
33+
)(NotifyToastContainer);

velog-frontend/src/containers/write/SubmitBoxContainer.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import WriteConfigureThumbnail from 'components/write/WriteConfigureThumbnail';
88
import SubmitBoxAdditional from 'components/write/SubmitBoxAdditional/SubmitBoxAdditional';
99
import { connect } from 'react-redux';
1010
import type { State } from 'store';
11-
import { WriteActions, UserActions } from 'store/actionCreators';
11+
import { WriteActions, UserActions, BaseActions } from 'store/actionCreators';
1212
import type { Categories, PostData, Meta } from 'store/modules/write';
1313
import axios from 'axios';
1414
import storage from 'lib/storage';
@@ -159,6 +159,10 @@ class SubmitBoxContainer extends Component<Props> {
159159
meta,
160160
url_slug: urlSlug,
161161
});
162+
BaseActions.showToast({
163+
type: 'success',
164+
message: '포스트가 수정됐습니다.',
165+
});
162166
} else {
163167
await WriteActions.writePost({
164168
title,
@@ -171,6 +175,10 @@ class SubmitBoxContainer extends Component<Props> {
171175
meta,
172176
urlSlug: urlSlug || escapeForUrl(title),
173177
});
178+
BaseActions.showToast({
179+
type: 'success',
180+
message: '포스트가 작성됐습니다.',
181+
});
174182
}
175183
} catch (e) {
176184
console.log(e);

velog-frontend/src/store/modules/base.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const SET_FULLSCREEN_LOADER = 'base/SET_FULLSCREEN_LOADER';
88
const ENTER_LANDING = 'base/ENTER_LANDING';
99
const EXIT_LANDING = 'base/EXIT_LANDING';
1010
const SET_WIDTH = 'base/SET_WIDTH';
11+
const SHOW_TOAST = 'base/SHOW_TOAST';
12+
const HIDE_TOAST = 'base/HIDE_TOAST';
1113

1214
const showUserMenu = createAction(SHOW_USER_MENU);
1315
const hideUserMenu = createAction(HIDE_USER_MENU);
@@ -18,11 +20,14 @@ const setFullscreenLoader = createAction(
1820
const exitLanding = createAction(EXIT_LANDING);
1921
const enterLanding = createAction(ENTER_LANDING);
2022
const setWidth = createAction(SET_WIDTH, (width: number) => width);
23+
const showToast = createAction(SHOW_TOAST, (payload: { type: string, message: string }) => payload);
24+
const hideToast = createAction(HIDE_TOAST);
2125

2226
type ShowUserMenuAction = ActionType<typeof showUserMenu>;
2327
type HideUserMenuAction = ActionType<typeof hideUserMenu>;
2428
type SetFullscreenLoaderAction = ActionType<typeof setFullscreenLoader>;
2529
type SetWidthAction = ActionType<typeof setWidth>;
30+
type ShowToastAction = ActionType<typeof showToast>;
2631

2732
export interface BaseActionCreators {
2833
showUserMenu(): any;
@@ -31,6 +36,8 @@ export interface BaseActionCreators {
3136
exitLanding(): any;
3237
enterLanding(): any;
3338
setWidth(width: number): any;
39+
showToast(payload: { type: string, message: string }): any;
40+
hideToast(): any;
3441
}
3542

3643
export const actionCreators: BaseActionCreators = {
@@ -40,20 +47,28 @@ export const actionCreators: BaseActionCreators = {
4047
exitLanding,
4148
enterLanding,
4249
setWidth,
50+
showToast,
51+
hideToast,
4352
};
4453

4554
export type Base = {
4655
userMenu: boolean,
4756
fullscreenLoader: boolean,
4857
landing: boolean,
4958
windowWidth: number,
59+
toast: { type: ?string, message: ?string, visible: boolean },
5060
};
5161

5262
const initialState: Base = {
5363
userMenu: false,
5464
fullscreenLoader: false,
5565
landing: true,
5666
windowWidth: 1920,
67+
toast: {
68+
type: null,
69+
message: null,
70+
visible: false,
71+
},
5772
};
5873

5974
export default handleActions(
@@ -86,6 +101,20 @@ export default handleActions(
86101
draft.windowWidth = action.payload;
87102
});
88103
},
104+
[SHOW_TOAST]: (state, { payload }: ShowToastAction) => {
105+
return {
106+
...state,
107+
toast: {
108+
...payload,
109+
visible: true,
110+
},
111+
};
112+
},
113+
[HIDE_TOAST]: (state) => {
114+
return produce(state, (draft) => {
115+
draft.toast.visible = false;
116+
});
117+
},
89118
},
90119
initialState,
91120
);

0 commit comments

Comments
 (0)