From 4eaf9659d18345dfefdd33c342c0dc51f5ddc4c6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 24 May 2021 14:54:46 +0200 Subject: [PATCH] UI notification/alert system with global redux state --- client/src/App.tsx | 3 ++ .../src/components/Apps/AppForm/AppForm.tsx | 2 +- client/src/components/Home/Home.module.css | 4 +- .../NotificationCenter.module.css | 8 ++++ .../NotificationCenter/NotificationCenter.tsx | 38 +++++++++++++++ .../UI/Notification/Notification.module.css | 47 +++++++++++++++++++ .../UI/Notification/Notification.tsx | 42 +++++++++++++++++ client/src/index.css | 1 + client/src/interfaces/GlobalState.ts | 2 + client/src/interfaces/Notification.ts | 8 ++++ client/src/interfaces/index.ts | 3 +- client/src/store/actions/actionTypes.ts | 15 ++++-- client/src/store/actions/app.ts | 37 ++++++++++++++- client/src/store/actions/index.ts | 3 +- client/src/store/actions/notification.ts | 27 +++++++++++ client/src/store/reducers/index.ts | 4 +- client/src/store/reducers/notification.ts | 45 ++++++++++++++++++ 17 files changed, 279 insertions(+), 10 deletions(-) create mode 100644 client/src/components/NotificationCenter/NotificationCenter.module.css create mode 100644 client/src/components/NotificationCenter/NotificationCenter.tsx create mode 100644 client/src/components/UI/Notification/Notification.module.css create mode 100644 client/src/components/UI/Notification/Notification.tsx create mode 100644 client/src/interfaces/Notification.ts create mode 100644 client/src/store/actions/notification.ts create mode 100644 client/src/store/reducers/notification.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 6a13241..2d7fef5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,6 +12,8 @@ import Apps from './components/Apps/Apps'; import Settings from './components/Settings/Settings'; import Bookmarks from './components/Bookmarks/Bookmarks'; +import NotificationCenter from './components/NotificationCenter/NotificationCenter'; + if (localStorage.theme) { store.dispatch(setTheme(localStorage.theme)); } @@ -27,6 +29,7 @@ const App = (): JSX.Element => { + ); } diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 0674cd0..dd3c85d 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, ChangeEvent, SyntheticEvent } from 'react'; import { connect } from 'react-redux'; import { addApp, updateApp } from '../../../store/actions'; -import { App, NewApp } from '../../../interfaces/App'; +import { App, NewApp } from '../../../interfaces'; import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; diff --git a/client/src/components/Home/Home.module.css b/client/src/components/Home/Home.module.css index 09d4d0c..05e3426 100644 --- a/client/src/components/Home/Home.module.css +++ b/client/src/components/Home/Home.module.css @@ -25,8 +25,8 @@ background-color: var(--color-accent); border-radius: 50%; position: fixed; - bottom: 10px; - left: 10px; + bottom: var(--spacing-ui); + left: var(--spacing-ui); display: flex; justify-content: center; align-items: center; diff --git a/client/src/components/NotificationCenter/NotificationCenter.module.css b/client/src/components/NotificationCenter/NotificationCenter.module.css new file mode 100644 index 0000000..8b8b88b --- /dev/null +++ b/client/src/components/NotificationCenter/NotificationCenter.module.css @@ -0,0 +1,8 @@ +.NotificationCenter { + position: fixed; + right: var(--spacing-ui); + bottom: var(--spacing-ui); + max-width: 300px; + z-index: 500; + color: white; +} \ No newline at end of file diff --git a/client/src/components/NotificationCenter/NotificationCenter.tsx b/client/src/components/NotificationCenter/NotificationCenter.tsx new file mode 100644 index 0000000..29c9cb2 --- /dev/null +++ b/client/src/components/NotificationCenter/NotificationCenter.tsx @@ -0,0 +1,38 @@ +import { connect } from 'react-redux'; +import { GlobalState, Notification as _Notification } from '../../interfaces'; + +import classes from './NotificationCenter.module.css'; + +import Notification from '../UI/Notification/Notification'; + +interface ComponentProps { + notifications: _Notification[]; +} + +const NotificationCenter = (props: ComponentProps): JSX.Element => { + return ( +
+ {props.notifications.map((notification: _Notification) => { + return ( + + ) + })} +
+ ) +} + +const mapStateToProps = (state: GlobalState) => { + return { + notifications: state.notification.notifications + } +} + +export default connect(mapStateToProps)(NotificationCenter); \ No newline at end of file diff --git a/client/src/components/UI/Notification/Notification.module.css b/client/src/components/UI/Notification/Notification.module.css new file mode 100644 index 0000000..2bac569 --- /dev/null +++ b/client/src/components/UI/Notification/Notification.module.css @@ -0,0 +1,47 @@ +.Notification { + width: 300px; + background-color: var(--color-background); + border: 1px solid var(--color-primary); + color: var(--color-primary); + border-radius: 4px; + padding: 15px 10px; + transition: all 0.25s; + margin-bottom: 10px; +} + +.Notification:hover { + background-color: var(--color-primary); + color: var(--color-background); + cursor: pointer; +} + +.Notification:last-child { + margin-bottom: 0; +} + +.NotificationOpen { + animation: slideIn 0.3s; +} + +.NotificationClose { + animation: slideOut 0.3s; + transform: translateX(600px); +} + +@keyframes slideIn { + from { + transform: translateX(600px); + } + to { + transform: translateX(0); + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + } + to { + transform: translateX(600px); + } +} \ No newline at end of file diff --git a/client/src/components/UI/Notification/Notification.tsx b/client/src/components/UI/Notification/Notification.tsx new file mode 100644 index 0000000..95109e1 --- /dev/null +++ b/client/src/components/UI/Notification/Notification.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { clearNotification } from '../../../store/actions'; + +import classes from './Notification.module.css'; + +interface ComponentProps { + title: string; + message: string; + id: number; + clearNotification: (id: number) => void; +} + +const Notification = (props: ComponentProps): JSX.Element => { + const [isOpen, setIsOpen] = useState(true); + const elementClasses = [classes.Notification, isOpen ? classes.NotificationOpen : classes.NotificationClose].join(' '); + + useEffect(() => { + const closeNotification = setTimeout(() => { + setIsOpen(false); + }, 3500); + + const clearNotification = setTimeout(() => { + props.clearNotification(props.id); + }, 3600) + + return () => { + window.clearTimeout(closeNotification); + window.clearTimeout(clearNotification); + } + }, []) + + return ( +
+

{props.title}

+

{props.message}

+
+
+ ) +} + +export default connect(null, { clearNotification })(Notification); \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 5f82a6f..a9c6013 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -10,6 +10,7 @@ body { --color-background: #2B2C56; --color-primary: #EFF1FC; --color-accent: #6677EB; + --spacing-ui: 10px; background-color: var(--color-background); transition: background-color 0.3s; diff --git a/client/src/interfaces/GlobalState.ts b/client/src/interfaces/GlobalState.ts index 8a3c10b..1ef7acb 100644 --- a/client/src/interfaces/GlobalState.ts +++ b/client/src/interfaces/GlobalState.ts @@ -1,9 +1,11 @@ import { State as AppState } from '../store/reducers/app'; import { State as ThemeState } from '../store/reducers/theme'; import { State as BookmarkState } from '../store/reducers/bookmark'; +import { State as NotificationState } from '../store/reducers/notification'; export interface GlobalState { theme: ThemeState; app: AppState; bookmark: BookmarkState; + notification: NotificationState; } \ No newline at end of file diff --git a/client/src/interfaces/Notification.ts b/client/src/interfaces/Notification.ts new file mode 100644 index 0000000..80a49f2 --- /dev/null +++ b/client/src/interfaces/Notification.ts @@ -0,0 +1,8 @@ +export interface NewNotification { + title: string; + message: string; +} + +export interface Notification extends NewNotification { + id: number; +} \ No newline at end of file diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 79078bd..1daf5b4 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -4,4 +4,5 @@ export * from './GlobalState'; export * from './Api'; export * from './Weather'; export * from './Bookmark'; -export * from './Category'; \ No newline at end of file +export * from './Category'; +export * from './Notification'; \ No newline at end of file diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index b2e46ad..153e3fa 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -10,7 +10,10 @@ import { // Categories GetCategoriesAction, AddCategoryAction, - AddBookmarkAction + AddBookmarkAction, + // Notifications + CreateNotificationAction, + ClearNotificationAction } from './'; export enum ActionTypes { @@ -30,7 +33,10 @@ export enum ActionTypes { getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', getCategoriesError = 'GET_CATEGORIES_ERROR', addCategory = 'ADD_CATEGORY', - addBookmark = 'ADD_BOOKMARK' + addBookmark = 'ADD_BOOKMARK', + // Notifications + createNotification = 'CREATE_NOTIFICATION', + clearNotification = 'CLEAR_NOTIFICATION' } export type Action = @@ -45,4 +51,7 @@ export type Action = // Categories GetCategoriesAction | AddCategoryAction | - AddBookmarkAction; \ No newline at end of file + AddBookmarkAction | + // Notifications + CreateNotificationAction | + ClearNotificationAction; \ No newline at end of file diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index 0ba2c3f..f7eb3a7 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; import { App, ApiResponse, NewApp } from '../../interfaces'; +import { CreateNotificationAction } from './notification'; export interface GetAppsAction { type: ActionTypes.getApps | ActionTypes.getAppsSuccess | ActionTypes.getAppsError; @@ -36,9 +37,19 @@ export interface PinAppAction { export const pinApp = (app: App) => async (dispatch: Dispatch) => { try { - const { id, isPinned} = app; + const { id, isPinned, name } = app; const res = await axios.put>(`/api/apps/${id}`, { isPinned: !isPinned }); + const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen'; + + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `App ${name} ${status}` + } + }) + dispatch({ type: ActionTypes.pinApp, payload: res.data.data @@ -57,6 +68,14 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => { try { const res = await axios.post>('/api/apps', formData); + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `App ${formData.name} added` + } + }) + dispatch({ type: ActionTypes.addAppSuccess, payload: res.data.data @@ -75,6 +94,14 @@ export const deleteApp = (id: number) => async (dispatch: Dispatch) => { try { const res = await axios.delete>(`/api/apps/${id}`); + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: 'App deleted' + } + }) + dispatch({ type: ActionTypes.deleteApp, payload: id @@ -93,6 +120,14 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp try { const res = await axios.put>(`/api/apps/${id}`, formData); + dispatch({ + type: ActionTypes.createNotification, + payload: { + title: 'Success', + message: `App ${formData.name} updated` + } + }) + dispatch({ type: ActionTypes.updateApp, payload: res.data.data diff --git a/client/src/store/actions/index.ts b/client/src/store/actions/index.ts index 15ca1e3..78c86b3 100644 --- a/client/src/store/actions/index.ts +++ b/client/src/store/actions/index.ts @@ -1,4 +1,5 @@ export * from './theme'; export * from './app'; export * from './actionTypes'; -export * from './bookmark'; \ No newline at end of file +export * from './bookmark'; +export * from './notification'; \ No newline at end of file diff --git a/client/src/store/actions/notification.ts b/client/src/store/actions/notification.ts new file mode 100644 index 0000000..a4f17cf --- /dev/null +++ b/client/src/store/actions/notification.ts @@ -0,0 +1,27 @@ +import { Dispatch } from 'redux'; +import { ActionTypes } from '.'; +import { NewNotification } from '../../interfaces'; + +export interface CreateNotificationAction { + type: ActionTypes.createNotification, + payload: NewNotification +} + +export const createNotification = (notification: NewNotification) => (dispatch: Dispatch) => { + dispatch({ + type: ActionTypes.createNotification, + payload: notification + }) +} + +export interface ClearNotificationAction { + type: ActionTypes.clearNotification, + payload: number +} + +export const clearNotification = (id: number) => (dispatch: Dispatch) => { + dispatch({ + type: ActionTypes.clearNotification, + payload: id + }) +} \ No newline at end of file diff --git a/client/src/store/reducers/index.ts b/client/src/store/reducers/index.ts index 16b9df7..bb0a0d6 100644 --- a/client/src/store/reducers/index.ts +++ b/client/src/store/reducers/index.ts @@ -5,11 +5,13 @@ import { GlobalState } from '../../interfaces/GlobalState'; import themeReducer from './theme'; import appReducer from './app'; import bookmarkReducer from './bookmark'; +import notificationReducer from './notification'; const rootReducer = combineReducers({ theme: themeReducer, app: appReducer, - bookmark: bookmarkReducer + bookmark: bookmarkReducer, + notification: notificationReducer }) export default rootReducer; \ No newline at end of file diff --git a/client/src/store/reducers/notification.ts b/client/src/store/reducers/notification.ts new file mode 100644 index 0000000..c0822b7 --- /dev/null +++ b/client/src/store/reducers/notification.ts @@ -0,0 +1,45 @@ +import { ActionTypes, Action } from '../actions'; +import { Notification } from '../../interfaces'; + +export interface State { + notifications: Notification[]; + idCounter: number; +} + +const initialState: State = { + notifications: [], + idCounter: 0 +} + +const createNotification = (state: State, action: Action): State => { + const tmpNotifications = [...state.notifications, { + ...action.payload, + id: state.idCounter + }]; + + return { + ...state, + notifications: tmpNotifications, + idCounter: state.idCounter + 1 + } +} + +const clearNotification = (state: State, action: Action): State => { + const tmpNotifications = [...state.notifications] + .filter((notification: Notification) => notification.id !== action.payload); + + return { + ...state, + notifications: tmpNotifications + } +} + +const notificationReducer = (state = initialState, action: Action) => { + switch (action.type) { + case ActionTypes.createNotification: return createNotification(state, action); + case ActionTypes.clearNotification: return clearNotification(state, action); + default: return state; + } +} + +export default notificationReducer; \ No newline at end of file