diff --git a/package-lock.json b/package-lock.json index d108fa4..51359dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "react-dom": "^19.1.1", "react-dropzone": "^14.3.8", "react-i18next": "^15.7.2", + "react-infinite-scroll-component": "^6.1.1", "react-leaflet": "^5.0.0-rc.2", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", @@ -4727,6 +4728,18 @@ } } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.1.tgz", + "integrity": "sha512-R8YoOyiNDynSWmfVme5LHslsKrP+/xcRUWR2ies8UgUab9dtyw5ECnMCVPPmnmjjF4MWQmfVdRwRWcWaDgeyMA==", + "license": "MIT", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5157,6 +5170,15 @@ "node": ">=18" } }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tiny-case": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", diff --git a/package.json b/package.json index 3a8e993..00abcc1 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-dom": "^19.1.1", "react-dropzone": "^14.3.8", "react-i18next": "^15.7.2", + "react-infinite-scroll-component": "^6.1.1", "react-leaflet": "^5.0.0-rc.2", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", diff --git a/src/index.css b/src/index.css index ca0b1a5..ccac1a1 100644 --- a/src/index.css +++ b/src/index.css @@ -95,3 +95,10 @@ tbody tr:nth-child(odd) { -webkit-appearance: none; -moz-appearance: textfield; } + +.modalGlass3 { + background: rgba(62, 61, 61, 0.2); + background-blend-mode: multiply; + + backdrop-filter: blur(44px); +} diff --git a/src/pages/notification/Notification.tsx b/src/pages/notification/Notification.tsx new file mode 100644 index 0000000..6ae2c58 --- /dev/null +++ b/src/pages/notification/Notification.tsx @@ -0,0 +1,171 @@ +import { Notification } from 'iconsax-react'; +import { type FC, useState } from 'react'; +import XIcon from '../../assets/images/close-circle.svg'; +import { useTranslation } from 'react-i18next'; +import { clx } from '../../helpers/utils'; +import StatusCircle from '../../components/StatusCircle'; +import { useOutsideClick } from '../../hooks/useOutSideClick'; +import { useGetNotification, useReadAll } from './hooks/useNotificationData'; +import { type NotificationItemType } from './types/NotificationTypes'; +import { timeAgo } from '../../config/func'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import MoonLoader from "react-spinners/MoonLoader" +import Button from '../../components/Button'; +import { toast } from 'react-toastify'; + +const Notifications: FC = () => { + // const navigate = useNavigate() + const { t } = useTranslation('global'); + const ref = useOutsideClick(() => setShowModal(false)); + const [activeTab, setActiveTab] = useState<'read' | 'unread'>('unread'); + const [showModal, setShowModal] = useState(false); + const { data, fetchNextPage, hasNextPage, refetch } = useGetNotification(activeTab); + const readAll = useReadAll() + const posts = data?.pages.flatMap((page) => page.data?.notifications || []) || []; + // const getDashboard = useGetDashboard() + + const handleAllRead = () => { + readAll.mutate(undefined, { + onSuccess: () => { + // getDashboard.refetch() + refetch() + toast.success(t('success')) + } + }) + } + + const handleRedirect = () => { + // switch (type) { + // case NotificationTypeEnum.USER_LOGIN: + // break; + // case NotificationTypeEnum.ANNOUNCEMENT: + // navigate(Pages.announcement.list) + // break; + // case NotificationTypeEnum.WALLET_CHARGE: + // navigate(Pages.transactions) + // break; + // case NotificationTypeEnum.WALLET_DEDUCTION: + // navigate(Pages.transactions) + // break; + // case NotificationTypeEnum.BILL_INVOICE_REMINDER: + // navigate(Pages.receipts.index) + // break; + // case NotificationTypeEnum.BILL_INVOICE: + // navigate(Pages.receipts.index) + // break; + // case NotificationTypeEnum.CREATE_INVOICE: + // navigate(Pages.receipts.index) + // break; + // case NotificationTypeEnum.CREATE_SERVICE: + // navigate(Pages.services.other) + // break; + // case NotificationTypeEnum.UNBLOCK_SERVICE: + // navigate(Pages.services.other) + // break; + // case NotificationTypeEnum.BLOCK_SERVICE: + // navigate(Pages.services.other) + // break; + // case NotificationTypeEnum.ANSWER_TICKET: + // navigate(Pages.ticket.list) + // break; + // case NotificationTypeEnum.CREATE_TICKET: + // navigate(Pages.ticket.list) + // break; + // default: + // break + // } + setShowModal(false) + } + + return ( +
+
setShowModal(!showModal)} className='relative cursor-pointer'> + +
+ {/* {getDashboard.data?.data?.notificationCount} */} +
+
+ + {showModal && ( +
+
+
{t('notif.natification')}
+
+ close setShowModal(false)} /> +
+
+ + + +
+
setActiveTab('unread')} + className={clx( + 'flex-1 border-l cursor-pointer text-white border-white border-opacity-70 bg-transparent flex justify-center items-center', + activeTab === 'unread' ? 'bg-white bg-opacity-70 text-black' : '' + )} + > + خوانده نشده +
+
setActiveTab('read')} + className={clx( + 'flex-1 cursor-pointer text-white border-white bg-transparent flex justify-center items-center', + activeTab === 'read' ? 'bg-white bg-opacity-70 text-black' : '' + )} + > + خوانده شده +
+
+ +
+ +
+ +
+
} + scrollableTarget="notificationsContainer" + > + { + posts.map((item: NotificationItemType) => ( +
handleRedirect()} className="bg-white cursor-pointer h-fit gap-4 items-start rounded-xl bg-opacity-60 p-3 mb-4 flex" key={item.id}> + { + !item.isRead && +
+ +
+ } +
+
{item.message}
+
+
{timeAgo(item.createdAt)}
+
+ +
{item.title}
+
+
+
+
+ )) + } + +
+
+ )} + + ); +}; + +export default Notifications; \ No newline at end of file diff --git a/src/pages/notification/hooks/useNotificationData.ts b/src/pages/notification/hooks/useNotificationData.ts new file mode 100644 index 0000000..40eeb97 --- /dev/null +++ b/src/pages/notification/hooks/useNotificationData.ts @@ -0,0 +1,26 @@ +import * as api from "../service/NotificationService"; +import { useInfiniteQuery, useMutation } from "@tanstack/react-query"; + +export const useGetNotification = (status: "all" | "read" | "unread") => { + return useInfiniteQuery({ + queryKey: ["notifications", status], + queryFn: ({ pageParam = 1 }) => api.getNotifications(pageParam, status), // فراخوانی API برای گرفتن داده‌ها + initialPageParam: 1, // صفحه اول به طور پیشفرض + getNextPageParam: (lastPage) => { + const currentPage = lastPage.data?.pager?.page; + const totalPages = lastPage.data?.pager?.totalPages; + + if (!currentPage || !totalPages) { + return undefined; + } + + return currentPage < totalPages ? currentPage + 1 : undefined; + }, + }); +}; + +export const useReadAll = () => { + return useMutation({ + mutationFn: () => api.readAll(), + }); +}; diff --git a/src/pages/notification/service/NotificationService.ts b/src/pages/notification/service/NotificationService.ts new file mode 100644 index 0000000..abc4405 --- /dev/null +++ b/src/pages/notification/service/NotificationService.ts @@ -0,0 +1,21 @@ +import axios from "../../../config/axios"; +import type { GetNotificationsResponse } from "../types/NotificationTypes"; + +export const getNotifications = async ( + page: number, + status: "all" | "read" | "unread" +): Promise => { + let query = ``; + if (status !== "all") { + query = `&isRead=${status === "read" ? 1 : 0}`; + } + const { data } = await axios.get( + `/admin/notifications?page=${page}${query}` + ); + return data; +}; + +export const readAll = async () => { + const { data } = await axios.patch(`/admin/notifications/read-all`); + return data; +}; diff --git a/src/pages/notification/types/NotificationTypes.ts b/src/pages/notification/types/NotificationTypes.ts new file mode 100644 index 0000000..2d91060 --- /dev/null +++ b/src/pages/notification/types/NotificationTypes.ts @@ -0,0 +1,45 @@ +import type { IResponse } from "@/types/response.types"; + +export type NotificationItemType = { + id: string; + title: string; + message: string; + createdAt: string; + isRead: boolean; + type: NotificationTypeEnum; +}; + +export type NotificationPager = { + page: number; + totalPages: number; +}; + +export type NotificationData = { + notifications: NotificationItemType[]; + pager: NotificationPager; +}; + +export type GetNotificationsResponse = IResponse; + +export enum NotificationTypeEnum { + USER_LOGIN = "USER_LOGIN", + ANNOUNCEMENT = "ANNOUNCEMENT", + + // Finance category + WALLET_CHARGE = "WALLET_CHARGE", + WALLET_DEDUCTION = "WALLET_DEDUCTION", + + //Invoice + BILL_INVOICE_REMINDER = "BILL_INVOICE_REMINDER", + BILL_INVOICE = "BILL_INVOICE", + CREATE_INVOICE = "CREATE_INVOICE", + + // Service category + CREATE_SERVICE = "CREATE_SERVICE", + UNBLOCK_SERVICE = "UNBLOCK_SERVICE", + BLOCK_SERVICE = "BLOCK_SERVICE", + + // Support category + ANSWER_TICKET = "ANSWER_TICKET", + CREATE_TICKET = "CREATE_TICKET", +} diff --git a/src/shared/Header.tsx b/src/shared/Header.tsx index 9942a9d..5cd97bf 100644 --- a/src/shared/Header.tsx +++ b/src/shared/Header.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next' import { useSharedStore } from './store/sharedStore' import { HambergerMenu } from 'iconsax-react' import { clx } from '@/helpers/utils' +import Notifications from '@/pages/notification/Notification' const Header: FC = () => { @@ -45,7 +46,7 @@ const Header: FC = () => { {/* */} - {/* */} + {/* { data && (