diff --git a/frontend/src/bundles/common/middlewares/error-handling.middleware.ts b/frontend/src/bundles/common/middlewares/error-handling.middleware.ts new file mode 100644 index 000000000..a6f150fae --- /dev/null +++ b/frontend/src/bundles/common/middlewares/error-handling.middleware.ts @@ -0,0 +1,39 @@ +import { + type Middleware, + isRejected, + isRejectedWithValue, +} from '@reduxjs/toolkit'; +import { type ServerValidationErrorResponse } from 'shared'; + +import { notificationService } from '../services/services.js'; + +const notificationId = 'redux-store-error'; + +const errorMiddleware: Middleware = () => { + return (next) => (action) => { + let message: string = ''; + if (isRejectedWithValue(action)) { + message += JSON.stringify(action.payload); + } else if (isRejected(action)) { + const error = action.error as ServerValidationErrorResponse; + message += `${error.message}\n`; + if (error.details) { + for (const errorDetail of error.details) { + message += `\t- ${errorDetail.message}\n`; + } + } + } + + if (message && !notificationService.isActive(notificationId)) { + notificationService.error({ + message, + id: notificationId, + title: 'An error occurred.', + }); + } + + return next(action); + }; +}; + +export { errorMiddleware }; diff --git a/frontend/src/bundles/common/middlewares/middlewares.ts b/frontend/src/bundles/common/middlewares/middlewares.ts new file mode 100644 index 000000000..6f713a537 --- /dev/null +++ b/frontend/src/bundles/common/middlewares/middlewares.ts @@ -0,0 +1 @@ +export { errorMiddleware } from './error-handling.middleware.js'; diff --git a/frontend/src/bundles/common/services/notification/notification.service.ts b/frontend/src/bundles/common/services/notification/notification.service.ts new file mode 100644 index 000000000..8c5269ed0 --- /dev/null +++ b/frontend/src/bundles/common/services/notification/notification.service.ts @@ -0,0 +1,91 @@ +import { type createStandaloneToast } from '@chakra-ui/react'; + +type Constructor = { + toast: ReturnType['toast']; +}; + +type NotifyProperties = { + message: string; + id: string; + title: string; + status?: 'info' | 'warning' | 'success' | 'error' | 'loading'; +}; + +class NotificationService { + private toast: ReturnType['toast']; + + public constructor({ toast }: Constructor) { + this.toast = toast; + } + + public warn = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'warning', + }); + }; + + public loading = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'loading', + }); + }; + + public error = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'error', + }); + }; + + public success = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'success', + }); + }; + + public info = ({ message, id, title }: NotifyProperties): void => { + this.showToastNotification({ + message, + id, + title, + status: 'info', + }); + }; + + public isActive = (id: string): boolean => { + return this.toast.isActive(id); + }; + + private showToastNotification = ({ + message, + id, + title, + status, + }: NotifyProperties): void => { + if (status) { + this.toast({ + id, + title, + description: message, + status, + duration: 7000, + isClosable: true, + position: 'top-right', + variant: 'solid', + }); + } + }; +} + +export { NotificationService }; diff --git a/frontend/src/bundles/common/services/notification/notification.ts b/frontend/src/bundles/common/services/notification/notification.ts new file mode 100644 index 000000000..fa7804cc1 --- /dev/null +++ b/frontend/src/bundles/common/services/notification/notification.ts @@ -0,0 +1,12 @@ +import { createStandaloneToast } from '@chakra-ui/react'; + +import { theme } from '~/framework/theme/theme.js'; + +import { NotificationService } from './notification.service.js'; + +const { toast } = createStandaloneToast({ theme: theme }); + +const notificationService = new NotificationService({ toast }); + +export { NotificationService } from './notification.service.js'; +export { notificationService }; diff --git a/frontend/src/bundles/common/services/services.ts b/frontend/src/bundles/common/services/services.ts new file mode 100644 index 000000000..e90e2c714 --- /dev/null +++ b/frontend/src/bundles/common/services/services.ts @@ -0,0 +1,4 @@ +export { + type NotificationService, + notificationService, +} from './notification/notification.js'; diff --git a/frontend/src/framework/store/store.package.ts b/frontend/src/framework/store/store.package.ts index 1bfb1225f..4c0f9515c 100644 --- a/frontend/src/framework/store/store.package.ts +++ b/frontend/src/framework/store/store.package.ts @@ -12,6 +12,8 @@ import { reducer as usersReducer } from '~/bundles/users/store/users.js'; import { userApi } from '~/bundles/users/users.js'; import { type Config } from '~/framework/config/config.js'; +import { errorMiddleware } from '../../bundles/common/middlewares/error-handling.middleware.js'; + type RootReducer = { auth: ReturnType; users: ReturnType; @@ -39,11 +41,12 @@ class Store { users: usersReducer, }, middleware: (getDefaultMiddleware) => { - return getDefaultMiddleware({ + const middlewares = getDefaultMiddleware({ thunk: { extraArgument: this.extraArguments, }, }); + return [...middlewares, errorMiddleware] as Tuple; }, }); }