diff --git a/pages/_app.tsx b/pages/_app.tsx index f8c3670..432591f 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,6 +12,7 @@ import { AnyAction, applyMiddleware, createStore, Middleware, Store } from 'redu import theme from 'src/common/presentation/components/theme'; import ConfirmContainer from 'src/common/presentation/container/molecules/ConfirmContainer'; import SnackbarContainer from 'src/common/presentation/container/molecules/SnackbarContainer'; +import NotificationCenterContainer from 'src/common/presentation/container/organisms/NotificationCenterContainer'; import CmsLayoutContainer from 'src/common/presentation/container/templates/CmsLayoutContainer'; import { setPaths } from 'src/common/presentation/state-module/common'; import { rootReducer, rootSaga, RootState } from 'src/common/presentation/state-module/root'; @@ -75,6 +76,7 @@ class MyApp extends App { + diff --git a/src/common/presentation/components/atmos/NotificationCenterButton.tsx b/src/common/presentation/components/atmos/NotificationCenterButton.tsx new file mode 100644 index 0000000..f699567 --- /dev/null +++ b/src/common/presentation/components/atmos/NotificationCenterButton.tsx @@ -0,0 +1,35 @@ +import { Button, createStyles, makeStyles } from '@material-ui/core'; +import { Notifications } from '@material-ui/icons'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import * as snackbarModule from '../../state-module/snackbar'; + +const useStyles = makeStyles(createStyles({ + icon: { + opacity: 0.8, + minWidth: 'initial' + } +})) + +interface Props { + dispatchers: typeof snackbarModule +} + +const NotificationCenterButton: React.FC = ({ dispatchers }) => { + const classes = useStyles(); + + return +} + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + dispatchers: bindActionCreators(snackbarModule, dispatch) +}) + +export default connect(null, mapDispatchToProps)(NotificationCenterButton); \ No newline at end of file diff --git a/src/common/presentation/components/organisms/NotificationCenter/NotificationCenter.tsx b/src/common/presentation/components/organisms/NotificationCenter/NotificationCenter.tsx new file mode 100644 index 0000000..f0da690 --- /dev/null +++ b/src/common/presentation/components/organisms/NotificationCenter/NotificationCenter.tsx @@ -0,0 +1,45 @@ +import { createStyles, Drawer, makeStyles } from '@material-ui/core'; +import * as React from 'react'; +import { Snackbar } from '../../../state-module/snackbar'; +import RenderNotification from './RenderNotification'; + +const useStyles = makeStyles(createStyles({ + drawerItems: { + width: 340 + }, + paper: { + background: 'rgba(70, 70, 70, 0.8)', + } +})) + +interface Props { + snackbars: Snackbar[] + opened: boolean + handleClose(): any + handleRemove(key: string): any +} + +const NotificationCenter: React.SFC = ({ snackbars, opened, handleClose, handleRemove }) => { + const classes = useStyles(); + + return +
+ {snackbars.map(s => + handleRemove(s.key)} + />)} +
+
+} + +export default NotificationCenter; \ No newline at end of file diff --git a/src/common/presentation/components/organisms/NotificationCenter/RenderNotification.tsx b/src/common/presentation/components/organisms/NotificationCenter/RenderNotification.tsx new file mode 100644 index 0000000..c62748b --- /dev/null +++ b/src/common/presentation/components/organisms/NotificationCenter/RenderNotification.tsx @@ -0,0 +1,146 @@ +import { Card, createStyles, Fade, makeStyles, Theme, useTheme } from '@material-ui/core'; +import { blue, green, grey, orange, red } from '@material-ui/core/colors'; +import { Cancel } from '@material-ui/icons'; +import { VariantType } from 'notistack'; +import Optional from 'optional-js'; +import * as React from 'react'; +import { Snackbar } from 'src/common/presentation/state-module/snackbar'; + +const useStyles = makeStyles((theme: Theme) => createStyles({ + card: { + margin: theme.spacing(1.5), + background: 'rgba(0, 0, 0, 0.35)', + color: theme.palette.primary.contrastText, + border: '0.5px solid #333', + cursor: 'default', + fontSize: '0.8rem' + }, + titleWrapper: { + background: 'rgba(0, 0, 0, 0.20)', + display: 'flex', + justifyContent: 'space-between' + }, + title: { + "& span:first-child": { + marginLeft: 7, + opacity: 0.9 + }, + "& span:not(:first-child)": { + color: grey[400], + margin: 3, + lineHeight: 2 + } + }, + notiContent: { + margin: `${theme.spacing(1.3)}px ${theme.spacing(2)}px` + }, + message: { + marginBottom: theme.spacing(1) + }, + messageOptions: { + color: grey[400], + whiteSpace: "pre" + }, + cancel: { + color: grey[400], + fontSize: '1rem', + margin: '5px 8px 0px 5px', + '&:active': { + color: "#fff" + } + } +})) + +interface Props { + snackbar: Snackbar + handleRemove(): any +} + +const colorMap = new Map(); +const colorWeight = 700; +colorMap.set("default", "initial"); +colorMap.set("error", red[colorWeight]); +colorMap.set("info", blue[colorWeight]); +colorMap.set("success", green[colorWeight]); +colorMap.set("warning", orange[colorWeight]); + +const RenderNotification: React.FC = ({ snackbar, handleRemove }) => { + const theme = useTheme(); + const classes = useStyles(); + + const options = Optional.ofNullable(snackbar.options); + + const variant = options.map(o => o.variant).orElse("default"); + const title = options.map(o => o.title).map(t => `: ${t}`).orElse(""); + + const [isCancelButtonHidden, setIsCancelButtonHidden] = React.useState(true); + const showCancelButton = () => setIsCancelButtonHidden(false); + const hideCancelButton = () => setIsCancelButtonHidden(true); + + const [checked, setChecked] = React.useState(true); + const fadeOut = () => setChecked(false); + + const [ref] = React.useState(React.createRef()); + const removeWithFadeoutAnimation = () => { + if (!ref.current) { + return; + } + + const { leavingScreen } = theme.transitions.duration; + + const initialHeight = ref.current.offsetHeight; + const deltaHeight = initialHeight / leavingScreen; + + const initialMargin = theme.spacing(1.5); + const deltaMargin = initialMargin / leavingScreen; + + ref.current.style.height = initialHeight + "px"; + ref.current.style.margin = initialMargin + "px"; + + // Remove element + setTimeout(handleRemove, leavingScreen); + fadeOut(); + + // Animation + for (let i = 0; i < leavingScreen; i++) { + setTimeout(() => { + if (!ref.current) { + return; + } + ref.current.style.height = (initialHeight - deltaHeight * i) + "px" + ref.current.style.margin = (initialMargin - deltaMargin * i) + "px" + }, i); + } + } + + return <> + + +
+
+ + {variant}{title} +
+ +
+
+
+ {snackbar.message} +
+
+ {Optional.ofNullable(snackbar.messageOptions).map((mo: any) => mo.e).orElse("")} +
+
+
+
+ ; +} + +export default RenderNotification; \ No newline at end of file diff --git a/src/common/presentation/components/organisms/NotificationCenter/index.tsx b/src/common/presentation/components/organisms/NotificationCenter/index.tsx new file mode 100644 index 0000000..017088c --- /dev/null +++ b/src/common/presentation/components/organisms/NotificationCenter/index.tsx @@ -0,0 +1 @@ +export { default } from './NotificationCenter' \ No newline at end of file diff --git a/src/common/presentation/components/templates/CmsLayout.tsx b/src/common/presentation/components/templates/CmsLayout.tsx index 15e31d2..0c048ed 100644 --- a/src/common/presentation/components/templates/CmsLayout.tsx +++ b/src/common/presentation/components/templates/CmsLayout.tsx @@ -22,6 +22,7 @@ import inversifyServices from 'src/inversifyServices'; import HorizontalMenuBarContainer from '../../container/molecules/HorizontalMenuBarContainer'; import LanguageToggleButton from '../atmos/LanguageToggleButton'; import Link from '../atmos/Link'; +import NotificationCenterButton from '../atmos/NotificationCenterButton'; const drawerWidth = 240; const horizontalMenuBarHeight = 31; /* manually calculate the height of horizonMenuBar */ @@ -163,7 +164,10 @@ const CmsLayout: React.FC = ({ children, t, paths, open, toggleOpen }) => {t('kiworkshop')} CMS - +
+ + +
diff --git a/src/common/presentation/container/molecules/SnackbarContainer.tsx b/src/common/presentation/container/molecules/SnackbarContainer.tsx index 1d085e6..8af9885 100644 --- a/src/common/presentation/container/molecules/SnackbarContainer.tsx +++ b/src/common/presentation/container/molecules/SnackbarContainer.tsx @@ -63,7 +63,7 @@ class SnackbarContainer extends React.Component { if (options.onClose) { options.onClose(event, reason, keyOfOption); } - }) as any + }) as any, }); // Keep track of snackbars that we've displayed this.storeDisplayed(key); diff --git a/src/common/presentation/container/organisms/NotificationCenterContainer.tsx b/src/common/presentation/container/organisms/NotificationCenterContainer.tsx new file mode 100644 index 0000000..790a3f4 --- /dev/null +++ b/src/common/presentation/container/organisms/NotificationCenterContainer.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import NotificationCenter from '../../components/organisms/NotificationCenter'; +import { RootState } from '../../state-module/root'; +import * as snackbarModule from '../../state-module/snackbar'; +import { Snackbar } from '../../state-module/snackbar'; + +interface Props { + isNotificationCenterOpened: boolean + snackbars: Snackbar[] + dispatchers: typeof snackbarModule +} + +const NotificationCenterContainer: React.FC = ({ + isNotificationCenterOpened: opened, + snackbars, + dispatchers, +}) => { + const handleClose = dispatchers.closeNotificationCenter; + const handleRemove = (key: string) => dispatchers.removeSnackbar({ key }); + + return +} + + +const mapStateToProps = ({ snackbar }: RootState) => ({ + isNotificationCenterOpened: snackbar.isNotificationCenterOpened, + snackbars: snackbar.snackbars +}) + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + dispatchers: bindActionCreators(snackbarModule, dispatch) +}) + +export default connect(mapStateToProps, mapDispatchToProps)(NotificationCenterContainer); \ No newline at end of file diff --git a/src/common/presentation/state-module/snackbar/snackbar.ts b/src/common/presentation/state-module/snackbar/snackbar.ts index 7f2e768..a6b3d69 100644 --- a/src/common/presentation/state-module/snackbar/snackbar.ts +++ b/src/common/presentation/state-module/snackbar/snackbar.ts @@ -5,11 +5,16 @@ import Optional from "optional-js"; import { ActionType, createReducer, createStandardAction, getType } from "typesafe-actions"; import uuid from 'uuid'; +export interface SnackbarOptionsObject extends OptionsObject { + onClose?: any + remove?(): void +} + export interface SnackbarEnqueuePayload { message: string | string[] messageOptions?: TOptions | string variant: VariantType - options?: OptionsObject | { onClose: any } + options?: SnackbarOptionsObject } export interface Snackbar { @@ -17,27 +22,35 @@ export interface Snackbar { message: string | string[] messageOptions?: TOptions | string dismissed?: boolean - options?: OptionsObject | { onClose: any } + options?: SnackbarOptionsObject } export const reset = createStandardAction("@snackbar/RESET")(); + export const enqueueSnackbar = createStandardAction("@snackbar/ENQUEUE_SNACKBAR")<{ snackbar: SnackbarEnqueuePayload }>(); export const dismissSnackbar = createStandardAction("@snackbar/DISMISS_SNACKBAR")<{ key: string }>(); export const removeSnackbar = createStandardAction("@snackbar/REMOVE_SNACKBAR")<{ key: string }>(); +export const openNotificationCenter = createStandardAction("@snackbar/OPEN_NOTIFICATION_CENTER")(); +export const closeNotificationCenter = createStandardAction("@snackbar/CLOSE_NOTIFICATION_CENTER")(); + export type Action = ActionType< typeof reset | typeof enqueueSnackbar | typeof dismissSnackbar | - typeof removeSnackbar + typeof removeSnackbar | + typeof openNotificationCenter | + typeof closeNotificationCenter > export interface State { snackbars: Snackbar[] + isNotificationCenterOpened: boolean } const createInitialState = (): State => ({ snackbars: [], + isNotificationCenterOpened: false }); export const reducer = createReducer(createInitialState()) @@ -64,4 +77,12 @@ export const reducer = createReducer(createInitialState()) const { key } = action.payload; draft.snackbars = draft.snackbars.filter(s => s.key !== key); return draft + })) + .handleAction(getType(openNotificationCenter), (state) => produce(state, draft => { + draft.isNotificationCenterOpened = true; + return draft; + })) + .handleAction(getType(closeNotificationCenter), (state) => produce(state, draft => { + draft.isNotificationCenterOpened = false; + return draft; })) \ No newline at end of file diff --git a/src/mother/notice/presentation/containers/NoticeDetailContainer.tsx b/src/mother/notice/presentation/containers/NoticeDetailContainer.tsx index 4caa471..a8e9b73 100644 --- a/src/mother/notice/presentation/containers/NoticeDetailContainer.tsx +++ b/src/mother/notice/presentation/containers/NoticeDetailContainer.tsx @@ -34,6 +34,7 @@ const NoticeDetailContainer: React.FC = ({ id, notice, pending, rejected, }, []) const { t } = useTranslation('noti'); + const deleteNotice = () => { commonDispatchers.openConfirmDialog({ "content": t("mother.notice.delete.confirm"),