diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index 04ffca2..7d89691 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -196,6 +196,7 @@ "name": "نام کامل", "username": "نام کاربری", "email": "پست الکترونیک", + "telephone_id": "کد تلفن", "phone_number": "شماره همراه", "national_id": "کد ملی", "position": "سمت", @@ -205,7 +206,7 @@ "male": "مرد", "female": "زن", "updated_at": "آخرین بروزرسانی", - "create": "افزودن کارشناس", + "create": "افزودن", "personal_info": "مشخصات کارشناس", "user_info": "اطلاعات کاربری", "rest_info": "اطلاعات تکمیلی", diff --git a/src/components/dashboard/edit-profile/index.jsx b/src/components/dashboard/edit-profile/index.jsx index e35e3bf..d15d467 100644 --- a/src/components/dashboard/edit-profile/index.jsx +++ b/src/components/dashboard/edit-profile/index.jsx @@ -7,19 +7,18 @@ import * as Yup from "yup"; import {Field, Formik} from "formik"; import {useTranslations} from "next-intl"; import AvatarUpload from "@/core/components/AvatarUpload"; -import axios from "axios"; import {UPDATE_AVATAR} from "@/core/data/apiRoutes"; import useDirection from "@/lib/app/hooks/useDirection"; -import Notifications from "@/core/components/notifications"; import ImageResizer from "@/core/components/ImageConvertor"; +import useRequest from "@/lib/app/hooks/useRequest"; const DashboardEditProfile = () => { const t = useTranslations(); + const requestServer = useRequest({auth: true}) const {user, token, getUser, changeUser} = useUser(); const {directionApp} = useDirection(); const editAvatar = async (avatar) => { - Notifications(t); try { const formData = new FormData(); @@ -28,15 +27,12 @@ const DashboardEditProfile = () => { resizedAvatar = await ImageResizer(avatar); formData.append("avatar", resizedAvatar); } - - await axios.post(UPDATE_AVATAR, formData, { - headers: { - authorization: `Bearer ${token}`, - "Content-Type": "multipart/form-data", - }, - }); + await requestServer(UPDATE_AVATAR, 'post', {formData}).then((response) => { + }) + .catch(() => { + }).finally(() => { + }); } catch (error) { - Notifications(t, error.response); throw error; } }; @@ -99,11 +95,7 @@ const DashboardEditProfile = () => { margin="normal" size="small" disabled={true} - error={ - props.touched.username && props.errors.username - ? true - : false - } + error={!!(props.touched.username && props.errors.username)} helperText={ props.touched.username ? props.errors.username @@ -123,11 +115,7 @@ const DashboardEditProfile = () => { margin="normal" size="small" disabled={true} - error={ - props.touched.name && props.errors.name - ? true - : false - } + error={!!(props.touched.name && props.errors.name)} helperText={ props.touched.name ? props.errors.name : null } @@ -145,11 +133,7 @@ const DashboardEditProfile = () => { margin="normal" size="small" disabled={true} - error={ - props.touched.email && props.errors.email - ? true - : false - } + error={!!(props.touched.email && props.errors.email)} helperText={ props.touched.email ? props.errors.email : null } @@ -168,12 +152,7 @@ const DashboardEditProfile = () => { margin="normal" size="small" disabled={true} - error={ - props.touched.province_name && - props.errors.province_name - ? true - : false - } + error={!!(props.touched.province_name && props.errors.province_name)} helperText={ props.touched.province_name ? props.errors.province_name @@ -193,11 +172,7 @@ const DashboardEditProfile = () => { margin="normal" size="small" disabled={true} - error={ - props.touched.position && props.errors.position - ? true - : false - } + error={!!(props.touched.position && props.errors.position)} helperText={ props.touched.position ? props.errors.position diff --git a/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js b/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js index 0ed26eb..cfc0a60 100644 --- a/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js +++ b/src/components/dashboard/expert-management/DataTable/__tests__/index.test.js @@ -10,6 +10,7 @@ describe("ExpertManagementDatatable Component From Expert Management", () => { const nameHeader = screen.queryByText("نام کامل"); const usernameHeader = screen.queryByText("نام کاربری"); const emailHeader = screen.queryByText("پست الکترونیک"); + const telephone_idHeader = screen.queryByText("کد تلفن"); const phone_numberHeader = screen.queryByText("شماره همراه"); const national_idHeader = screen.queryByText("کد ملی"); const positionHeader = screen.queryByText("سمت"); @@ -21,6 +22,7 @@ describe("ExpertManagementDatatable Component From Expert Management", () => { expect(nameHeader).toBeInTheDocument(); expect(usernameHeader).toBeInTheDocument(); expect(emailHeader).toBeInTheDocument(); + expect(telephone_idHeader).toBeInTheDocument(); expect(phone_numberHeader).toBeInTheDocument(); expect(national_idHeader).toBeInTheDocument(); expect(positionHeader).toBeInTheDocument(); diff --git a/src/components/dashboard/expert-management/DataTable/index.jsx b/src/components/dashboard/expert-management/DataTable/index.jsx index 3f76ca5..41dd1e6 100644 --- a/src/components/dashboard/expert-management/DataTable/index.jsx +++ b/src/components/dashboard/expert-management/DataTable/index.jsx @@ -67,6 +67,18 @@ function ExpertManagementDataTable() { {renderedCellValue} ), }, + { + accessorFn: (row) => row.telephone_id, + id: "telephone_id", + header: t("ExpertMangement.telephone_id"), + enableColumnFilter: true, + datatype: "numeric", + filterFn: "equals", + columnFilterModeOptions: ["contains", "equals", "notEquals"], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, { accessorFn: (row) => row.phone_number, id: "phone_number", diff --git a/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js b/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js index 8bf825d..dda7f82 100644 --- a/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js +++ b/src/components/dashboard/expert-management/Form/CreateForm/__tests__/index.test.js @@ -18,22 +18,22 @@ describe("CreateForm Component From Expert Management", () => { describe("Rendering", () => { it("Create Button Rendered", () => { render(); - const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"}); + const CreateButton = screen.getByRole('button', {name: "افزودن"}); expect(CreateButton).toBeInTheDocument(); }); it("Dialog Header Rendered", () => { render(); - const CreateDialogHeader = screen.queryByText("افزودن کارشناس"); + const CreateDialogHeader = screen.queryByText("افزودن"); expect(CreateDialogHeader).toBeInTheDocument(); }); }); describe("Behavioral", () => { it("by Clicking Create Button Dialog Should Append To Document", async () => { render(); - const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"}); + const CreateButton = screen.getByRole('button', {name: "افزودن"}); fireEvent.click(CreateButton); await act(() => { - const CreateDialog = screen.getByRole('dialog', {name: "افزودن کارشناس"}) + const CreateDialog = screen.getByRole('dialog', {name: "افزودن"}) expect(CreateDialog).toBeInTheDocument(); }); }); @@ -42,7 +42,7 @@ describe("CreateForm Component From Expert Management", () => { it('Should request to api and if get success close dialog', async () => { render(); - const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"}); + const CreateButton = screen.getByRole('button', {name: "افزودن"}); fireEvent.click(CreateButton); const submitButton = screen.queryByText('ثبت'); @@ -80,7 +80,7 @@ describe("CreateForm Component From Expert Management", () => { it('Should request to api and if get error keep previous data', async () => { render(); - const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"}); + const CreateButton = screen.getByRole('button', {name: "افزودن"}); fireEvent.click(CreateButton); const submitButton = screen.queryByText('ثبت'); diff --git a/src/core/components/notifications/ErrorNotification.jsx b/src/core/components/notifications/ErrorNotification.jsx index 05e2fb9..776c904 100644 --- a/src/core/components/notifications/ErrorNotification.jsx +++ b/src/core/components/notifications/ErrorNotification.jsx @@ -2,8 +2,8 @@ import DangerousIcon from "@mui/icons-material/Dangerous"; import {Box, Typography} from "@mui/material"; import {toast} from "react-toastify"; -const ErrorNotification = (t, status, message) => { - toast( +const ErrorNotification = (pushToastList, notificationType, t, status, message) => { + const toastId = toast( () => ( <> { ), { + containerId: 'validation', autoClose: false, closeOnClick: false, draggable: false, } ); + pushToastList(notificationType, toastId); }; export default ErrorNotification; \ No newline at end of file diff --git a/src/core/components/notifications/PendingNotification.jsx b/src/core/components/notifications/PendingNotification.jsx index 6183f6b..cc28a58 100644 --- a/src/core/components/notifications/PendingNotification.jsx +++ b/src/core/components/notifications/PendingNotification.jsx @@ -1,12 +1,14 @@ import {toast} from "react-toastify"; -const PendingNotification = (t) => { - toast(t("notifications.pending"), { +const PendingNotification = (pushToastList, notificationType, t) => { + const toastId = toast(t("notifications.pending"), { + containerId: 'validation', autoClose: false, closeButton: false, closeOnClick: false, draggable: false, }); + pushToastList(notificationType, toastId); }; export default PendingNotification; \ No newline at end of file diff --git a/src/core/components/notifications/SuccessNotification.jsx b/src/core/components/notifications/SuccessNotification.jsx index 3507cef..1207000 100644 --- a/src/core/components/notifications/SuccessNotification.jsx +++ b/src/core/components/notifications/SuccessNotification.jsx @@ -2,8 +2,8 @@ import BeenhereIcon from "@mui/icons-material/Beenhere"; import {Box, Typography} from "@mui/material"; import {toast} from "react-toastify"; -const SuccessNotification = (t, status) => { - toast( +const SuccessNotification = (pushToastList, notificationType, t, status) => { + const toastId = toast( () => ( <> { ), { + containerId: 'validation', autoClose: 3000, hideProgressBar: true, pauseOnHover: true, @@ -37,6 +38,7 @@ const SuccessNotification = (t, status) => { draggable: true, } ); + pushToastList(notificationType, toastId); }; export default SuccessNotification; \ No newline at end of file diff --git a/src/core/components/notifications/UploadFileNotification.jsx b/src/core/components/notifications/UploadFileNotification.jsx index 89ebbb2..1c20ba3 100644 --- a/src/core/components/notifications/UploadFileNotification.jsx +++ b/src/core/components/notifications/UploadFileNotification.jsx @@ -26,6 +26,8 @@ const UploadFileNotification = (t) => { ), { + containerId: 'validation', + toastId: 'upload', autoClose: 3000, hideProgressBar: true, pauseOnHover: true, diff --git a/src/core/components/notifications/WarningNotification.jsx b/src/core/components/notifications/WarningNotification.jsx index 19d6a2f..bc2a1fb 100644 --- a/src/core/components/notifications/WarningNotification.jsx +++ b/src/core/components/notifications/WarningNotification.jsx @@ -2,8 +2,8 @@ import ReportIcon from "@mui/icons-material/Report"; import {Box, Typography} from "@mui/material"; import {toast} from "react-toastify"; -const WarningNotification = (t, status) => { - toast( +const WarningNotification = (pushToastList, notificationType, t, status) => { + const toastId = toast( () => ( <> { ), { + containerId: 'validation', autoClose: false, closeOnClick: false, draggable: false, } ); + pushToastList(notificationType, toastId); }; export default WarningNotification; \ No newline at end of file diff --git a/src/core/components/notifications/index.jsx b/src/core/components/notifications/index.jsx index bc8eb66..34e44f3 100644 --- a/src/core/components/notifications/index.jsx +++ b/src/core/components/notifications/index.jsx @@ -1,54 +1,27 @@ -import {toast} from "react-toastify"; -import ErrorNotification from "./ErrorNotification"; -import WarningNotification from "./WarningNotification"; -import SuccessNotification from "./SuccessNotification"; +import pendingNotification from "@/core/components/notifications/PendingNotification"; +import WarningNotification from "@/core/components/notifications/WarningNotification"; +import ErrorNotification from "@/core/components/notifications/ErrorNotification"; +import SuccessNotification from "@/core/components/notifications/SuccessNotification"; -const Notifications = async (t, response) => { - const {status, data} = response != undefined ? response : "" - toast.dismiss(); - switch (status) { - case 200: - SuccessNotification(t, status); +const Notifications = (pushToastList, notificationType, t, status, message) => { + switch (notificationType) { + case "pending": + pendingNotification(pushToastList, notificationType, t); break; - case 400: - ErrorNotification(t, status); + case "warning": + WarningNotification(pushToastList, notificationType, t, status); break; - case 401: - ErrorNotification(t, status); + case "error": + if (message) { + ErrorNotification(pushToastList, notificationType, t, status, message) + } else { + ErrorNotification(pushToastList, notificationType, t, status) + } break; - case 403: - ErrorNotification(t, status); - break; - case 422: - ErrorNotification(t, status, data.message); - break; - case 500: - WarningNotification(t, status); - break; - case 503: - WarningNotification(t, status); - break; - case 504: - WarningNotification(t, status); - break; - default: - toast(t("notifications.pending"), { - autoClose: false, - closeOnClick: false, - draggable: false, - }); + case "success": + SuccessNotification(pushToastList, notificationType, t, status); break; } }; export default Notifications; - -/* -usage document - -** for pending use ( Notifications( t, undefined) ) this before your request. -** for success use ( Notifications( t, response) ) this inside .then() of your request. -** for Error and Warning use ( Notifications( t, error.response) ) this inside .catche() of your request. - -end usage document -*/ diff --git a/src/core/utils/errorHandler.js b/src/core/utils/errorHandler.js index 8dbd364..2c12ab2 100644 --- a/src/core/utils/errorHandler.js +++ b/src/core/utils/errorHandler.js @@ -1,45 +1,48 @@ -import ErrorNotification from "@/core/components/notifications/ErrorNotification"; -import WarningNotification from "@/core/components/notifications/WarningNotification"; -import {toast} from "react-toastify"; +import Notifications from "@/core/components/notifications"; -export const errorSetting = (t, notification) => { - if (notification) toast.dismiss(); +export const errorSetting = (dismissToastList, t, notification) => { + if (notification) { + dismissToastList(["pending", "warning", "error", "success"]) + } } - -export const errorRequest = (t, notification) => { - if (notification) toast.dismiss(); -} - -export const errorResponse = (response, clearToken, t, notification) => { - if (notification) toast.dismiss(); - if (isServerError(response.status)) { - errorServer(response, t, notification) - } else if (isClientError(response.status)) { - errorClient(response, clearToken, t, notification) +export const errorRequest = (dismissToastList, t, notification) => { + if (notification) { + dismissToastList(["pending", "warning", "error", "success"]) } } -const errorServer = (response, t, notification) => { - if (notification) WarningNotification(t, response.status); +export const errorResponse = (pushToastList, dismissToastList, response, clearToken, t, notification) => { + if (notification) { + dismissToastList(["pending", "warning", "error", "success"]) + } + if (isServerError(response.status)) { + errorServer(pushToastList, response, t, notification) + } else if (isClientError(response.status)) { + errorClient(pushToastList, response, clearToken, t, notification) + } } -const errorClient = (response, clearToken, t, notification) => { + +const errorServer = (pushToastList, response, t, notification) => { + if (notification) Notifications(pushToastList, "warning", t, response.status); +} +const errorClient = (pushToastList, response, clearToken, t, notification) => { switch (response.status) { case 401: clearToken() - if (notification) ErrorNotification(t, response.status) + if (notification) Notifications(pushToastList, "error", t, response.status); break; case 422: if ('type' in response.data) { - errorLogic(response, t, notification) + errorLogic(pushToastList, response, t, notification) break; } - errorValidation(response, t, notification) + errorValidation(pushToastList, response, t, notification) break; case 429: - if (notification) ErrorNotification(t, response.status) + if (notification) Notifications(pushToastList, "error", t, response.status); break default: - if (notification) ErrorNotification(t, response.status) + if (notification) Notifications(pushToastList, "error", t, response.status); break } } @@ -47,16 +50,16 @@ const errorClient = (response, clearToken, t, notification) => { const isServerError = status => status >= 500 && status <= 599; const isClientError = status => status >= 400 && status <= 499; -const errorLogic = (response, t, notification) => { - if (notification) ErrorNotification(t, response.status, response.data.message) +const errorLogic = (pushToastList, response, t, notification) => { + if (notification) Notifications(pushToastList, "error", t, response.status, response.data.message); } -const errorValidation = (response, t, notification) => { +const errorValidation = (pushToastList, response, t, notification) => { if (notification) { const errorsMap = Object.keys(response.data.errors) const errorsArray = response.data.errors errorsMap.map((item, index) => { - ErrorNotification(t, response.status, errorsArray[item][0]); + Notifications(pushToastList, "error", t, errorsArray[item][0], response.data.message); }) } } \ No newline at end of file diff --git a/src/core/utils/succesHandler.js b/src/core/utils/succesHandler.js index a61ac85..c852f86 100644 --- a/src/core/utils/succesHandler.js +++ b/src/core/utils/succesHandler.js @@ -1,9 +1,8 @@ -import SuccessNotification from "@/core/components/notifications/SuccessNotification"; -import {toast} from "react-toastify"; +import Notifications from "@/core/components/notifications"; -export const successRequest = (response, t, options) => { +export const successRequest = (pushToastList, dismissToastList, response, t, options) => { if (options.notification && options.success.notification.show) { - toast.dismiss(); - SuccessNotification(t, response.status) + dismissToastList(["pending", "warning", "error", "success"]) + Notifications(pushToastList, "success", t, response.status); } } \ No newline at end of file diff --git a/src/layouts/AppLayout.jsx b/src/layouts/AppLayout.jsx index 4fc5e1d..5bb1d23 100644 --- a/src/layouts/AppLayout.jsx +++ b/src/layouts/AppLayout.jsx @@ -41,16 +41,22 @@ function AppLayout({children, isBot}) { if (!languageIsReady) return; } - return (<> - - - - - {children} - ); + return ( + <> + + + + + {children} + + ); } export default AppLayout; diff --git a/src/lib/app/contexts/socket.jsx b/src/lib/app/contexts/socket.jsx index 7d51194..363faca 100644 --- a/src/lib/app/contexts/socket.jsx +++ b/src/lib/app/contexts/socket.jsx @@ -2,9 +2,10 @@ import {createContext, useEffect, useMemo, useRef, useState} from "react"; import {io} from "socket.io-client"; import useUser from "@/lib/app/hooks/useUser"; import {useTranslations} from "next-intl"; -import {toast} from "react-toastify"; +import {toast, ToastContainer} from "react-toastify"; import PowerIcon from "@mui/icons-material/Power"; import PowerOffIcon from "@mui/icons-material/PowerOff"; +import useDirection from "@/lib/app/hooks/useDirection"; export const SocketContext = createContext() @@ -13,6 +14,7 @@ export const SocketProvider = ({children}) => { const [connectionError, setConnectionError] = useState(false) const socketToastId = useRef(null); const t = useTranslations() + const {directionApp} = useDirection(); const socket = useMemo(() => io(process.env.NEXT_PUBLIC_SERVER_SOCKET_URL, { autoConnect: false, transports: ['websocket'], @@ -58,11 +60,20 @@ export const SocketProvider = ({children}) => { return } socketToastId.current = toast.warn(t('socket_is_not_connect_message'), { + containerId: 'socket', draggable: false, autoClose: false, closeButton: false, closeOnClick: false, icon: }) }, [connectionError]); - return {children} + return ( + + + {children} + + ) } \ No newline at end of file diff --git a/src/lib/app/contexts/toast.jsx b/src/lib/app/contexts/toast.jsx new file mode 100644 index 0000000..b9d4cbc --- /dev/null +++ b/src/lib/app/contexts/toast.jsx @@ -0,0 +1,37 @@ +import {createContext, useReducer} from "react"; +import {toast} from "react-toastify"; + +export const ToastContext = createContext() + +const reducer = (state, action) => { + switch (action.type) { + case "PUSH": + return { + ...state, + [action.toast_type]: [...state[action.toast_type], action.toast_id] + }; + case "DISMISS": + action.toast_type.map((item) => { + state[item].map((id) => { + toast.dismiss(id); + }) + state[item] = [] + }); + return state; + } +}; + +export const ToastProvider = ({children}) => { + const [state, dispatch] = useReducer(reducer, { + pending: [], + error: [], + warning: [], + success: [] + }); + + return ( + + {children} + + ); +}; diff --git a/src/lib/app/hooks/useRequest.jsx b/src/lib/app/hooks/useRequest.jsx index d361e08..4c3be1d 100644 --- a/src/lib/app/hooks/useRequest.jsx +++ b/src/lib/app/hooks/useRequest.jsx @@ -1,10 +1,11 @@ import axios from "axios"; import {successRequest} from "@/core/utils/succesHandler"; -import PendingNotification from "@/core/components/notifications/PendingNotification"; import {useTranslations} from "next-intl"; import useUser from "@/lib/app/hooks/useUser"; import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler"; import useNetwork from "@/lib/app/hooks/useNetwork"; +import Notifications from "@/core/components/notifications"; +import useToast from "@/lib/app/hooks/useToast"; const defaultOptions = { auth: false, data: {}, requestOptions: { @@ -23,6 +24,7 @@ const useRequest = (initOptions) => { const network = useNetwork() const t = useTranslations() const {token, clearToken} = useUser() + const {pushToastList, dismissToastList} = useToast(); let _options = {...defaultOptions, ...initOptions} function requestServer(url = '', method = 'get', options) { @@ -33,27 +35,30 @@ const useRequest = (initOptions) => { headers: {..._options.requestOptions.headers, authorization: `Bearer ${token}`} } } - return new Promise((resolve, reject) => { if (!network.online) { reject() return } - if (_options.notification && _options.failed.notification.show && _options.pending) PendingNotification(t) + if (_options.notification && _options.failed.notification.show && _options.pending) { + dismissToastList(["pending", "warning", "error", "success"]); + Notifications(pushToastList, "pending", t); + } + axios({ url: url, method: method, data: _options.data, ..._options.requestOptions }) .then(response => { - successRequest(response, t, _options) + successRequest(pushToastList, dismissToastList, response, t, _options) resolve(response) }) .catch(error => { if (error.response) { - errorResponse(error.response, clearToken, t, _options.notification && _options.failed.notification.show) + errorResponse(pushToastList, dismissToastList, error.response, clearToken, t, _options.notification && _options.failed.notification.show) } else if (error.request) { - errorRequest(t, _options.notification && _options.failed.notification.show) + errorRequest(dismissToastList, t, _options.notification && _options.failed.notification.show) } else { - errorSetting(t, _options.notification && _options.failed.notification.show) + errorSetting(dismissToastList, t, _options.notification && _options.failed.notification.show) } reject(error) }) diff --git a/src/lib/app/hooks/useToast.jsx b/src/lib/app/hooks/useToast.jsx new file mode 100644 index 0000000..5e1d5b4 --- /dev/null +++ b/src/lib/app/hooks/useToast.jsx @@ -0,0 +1,21 @@ +import {useContext} from "react"; +import {ToastContext} from "@/lib/app/contexts/toast"; + +const useToast = () => { + const {dispatch} = useContext(ToastContext); + + const pushToastList = (toast_type, toast_id) => { + dispatch({type: "PUSH", toast_type, toast_id}); + }; + + const dismissToastList = (toast_type) => { + dispatch({type: "DISMISS", toast_type}); + }; + + return { + pushToastList, + dismissToastList + } +}; + +export default useToast; \ No newline at end of file diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 4cd2a1d..2d23745 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -8,21 +8,24 @@ import "moment/locale/fa"; import {NextIntlProvider} from "next-intl"; import TitlePage from "@/core/components/TitlePage"; import {SocketProvider} from "@/lib/app/contexts/socket"; +import {ToastProvider} from "@/lib/app/contexts/toast"; const App = ({Component, pageProps}) => { return (<> - - - {pageProps.messages ? : ''} + + + {pageProps.messages ? : ''} - - - - - + + + + + + +