UI notification/alert system with global redux state

This commit is contained in:
unknown 2021-05-24 14:54:46 +02:00
parent c145888aec
commit 4eaf9659d1
17 changed files with 279 additions and 10 deletions

View file

@ -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<any>(setTheme(localStorage.theme));
}
@ -27,6 +29,7 @@ const App = (): JSX.Element => {
<Route path='/bookmarks' component={Bookmarks} />
</Switch>
</BrowserRouter>
<NotificationCenter />
</Provider>
);
}

View file

@ -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';

View file

@ -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;

View file

@ -0,0 +1,8 @@
.NotificationCenter {
position: fixed;
right: var(--spacing-ui);
bottom: var(--spacing-ui);
max-width: 300px;
z-index: 500;
color: white;
}

View file

@ -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 (
<div
className={classes.NotificationCenter}
style={{ height: `${props.notifications.length * 75}px` }}
>
{props.notifications.map((notification: _Notification) => {
return (
<Notification
title={notification.title}
message={notification.message}
id={notification.id}
key={notification.id}
/>
)
})}
</div>
)
}
const mapStateToProps = (state: GlobalState) => {
return {
notifications: state.notification.notifications
}
}
export default connect(mapStateToProps)(NotificationCenter);

View file

@ -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);
}
}

View file

@ -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 (
<div className={elementClasses}>
<h4>{props.title}</h4>
<p>{props.message}</p>
<div className={classes.Pog}></div>
</div>
)
}
export default connect(null, { clearNotification })(Notification);

View file

@ -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;

View file

@ -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;
}

View file

@ -0,0 +1,8 @@
export interface NewNotification {
title: string;
message: string;
}
export interface Notification extends NewNotification {
id: number;
}

View file

@ -4,4 +4,5 @@ export * from './GlobalState';
export * from './Api';
export * from './Weather';
export * from './Bookmark';
export * from './Category';
export * from './Category';
export * from './Notification';

View file

@ -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<any> |
AddCategoryAction |
AddBookmarkAction;
AddBookmarkAction |
// Notifications
CreateNotificationAction |
ClearNotificationAction;

View file

@ -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<T> {
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<ApiResponse<App>>(`/api/apps/${id}`, { isPinned: !isPinned });
const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen';
dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: {
title: 'Success',
message: `App ${name} ${status}`
}
})
dispatch<PinAppAction>({
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<ApiResponse<App>>('/api/apps', formData);
dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: {
title: 'Success',
message: `App ${formData.name} added`
}
})
dispatch<AddAppAction>({
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<ApiResponse<{}>>(`/api/apps/${id}`);
dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: {
title: 'Success',
message: 'App deleted'
}
})
dispatch<DeleteAppAction>({
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<ApiResponse<App>>(`/api/apps/${id}`, formData);
dispatch<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: {
title: 'Success',
message: `App ${formData.name} updated`
}
})
dispatch<UpdateAppAction>({
type: ActionTypes.updateApp,
payload: res.data.data

View file

@ -1,4 +1,5 @@
export * from './theme';
export * from './app';
export * from './actionTypes';
export * from './bookmark';
export * from './bookmark';
export * from './notification';

View file

@ -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<CreateNotificationAction>({
type: ActionTypes.createNotification,
payload: notification
})
}
export interface ClearNotificationAction {
type: ActionTypes.clearNotification,
payload: number
}
export const clearNotification = (id: number) => (dispatch: Dispatch) => {
dispatch<ClearNotificationAction>({
type: ActionTypes.clearNotification,
payload: id
})
}

View file

@ -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<GlobalState>({
theme: themeReducer,
app: appReducer,
bookmark: bookmarkReducer
bookmark: bookmarkReducer,
notification: notificationReducer
})
export default rootReducer;

View file

@ -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;