From 617041e2baf6ff61228d7c06441a5aaffff73588 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 8 Feb 2025 18:37:10 +0330 Subject: [PATCH] notifications --- .env | 4 +- package-lock.json | 22 ++ package.json | 1 + src/config/func.ts | 22 ++ src/pages/notification/Notification.tsx | 208 +++++++++--------- .../notification/hooks/useNotificationData.ts | 17 ++ .../service/NotificationService.ts | 6 + .../notification/types/NotificationTypes.ts | 7 + 8 files changed, 182 insertions(+), 105 deletions(-) create mode 100644 src/pages/notification/hooks/useNotificationData.ts create mode 100644 src/pages/notification/service/NotificationService.ts create mode 100644 src/pages/notification/types/NotificationTypes.ts diff --git a/.env b/.env index ed70d10..c7f554d 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ VITE_TOKEN_NAME = 'dsc_token' VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token' -VITE_BASE_URL = 'https://danak-dsc-api.run.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.0.221:4000' \ No newline at end of file +# VITE_BASE_URL = 'https://danak-dsc-api.run.danakcorp.com' +VITE_BASE_URL = 'http://192.168.0.223:3500' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eef4972..bf506af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react-dom": "^19.0.0", "react-dropzone": "^14.3.5", "react-i18next": "^15.2.0", + "react-infinite-scroll-component": "^6.1.0", "react-loading": "^2.0.3", "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", @@ -4391,6 +4392,18 @@ } } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "license": "MIT", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -4978,6 +4991,15 @@ "node": ">=0.8" } }, + "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 d41b5dd..c553ebd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-dom": "^19.0.0", "react-dropzone": "^14.3.5", "react-i18next": "^15.2.0", + "react-infinite-scroll-component": "^6.1.0", "react-loading": "^2.0.3", "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", diff --git a/src/config/func.ts b/src/config/func.ts index fdb3d58..09e5992 100644 --- a/src/config/func.ts +++ b/src/config/func.ts @@ -6,3 +6,25 @@ export function isEmail(input: string): boolean { export function NumberFormat(number: number): string { return Intl.NumberFormat("fa-IR").format(number); } + +export const timeAgo = (date: string | Date): string => { + const now = new Date(); + const past = new Date(date); + const diff = Math.floor((now.getTime() - past.getTime()) / 1000); // اختلاف بر حسب ثانیه + + const units = [ + { name: "سال", value: 60 * 60 * 24 * 365 }, + { name: "ماه", value: 60 * 60 * 24 * 30 }, + { name: "روز", value: 60 * 60 * 24 }, + { name: "ساعت", value: 60 * 60 }, + { name: "دقیقه", value: 60 }, + { name: "ثانیه", value: 1 }, + ]; + + for (const unit of units) { + const amount = Math.floor(diff / unit.value); + if (amount >= 1) return `${amount} ${unit.name} پیش`; + } + + return "همین الان"; +}; diff --git a/src/pages/notification/Notification.tsx b/src/pages/notification/Notification.tsx index 5682255..54789de 100644 --- a/src/pages/notification/Notification.tsx +++ b/src/pages/notification/Notification.tsx @@ -1,113 +1,115 @@ -import { Notification } from 'iconsax-react' -import { 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 { Notification } from 'iconsax-react'; +import { 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 } from './hooks/useNotificationData'; +import { NotificationItemType } from './types/NotificationTypes'; +import { timeAgo } from '../../config/func'; +import InfiniteScroll from 'react-infinite-scroll-component'; const Notifications: FC = () => { + const { t } = useTranslation('global'); + const ref = useOutsideClick(() => setShowModal(false)); + const [activeTab, setActiveTab] = useState<'all' | 'read' | 'unread'>('all'); + const [showModal, setShowModal] = useState(false); + const { data, fetchNextPage, hasNextPage, isFetching, isLoading } = useGetNotification(); - const { t } = useTranslation('global') - const ref = useOutsideClick(() => setShowModal(false)) - const [activeTab, setActiveTab] = useState<'all' | 'read' | 'unread'>('all') - const [showModal, setShowModal] = useState(false) + const posts = data?.pages.flatMap((page) => page.data?.notifications) || []; + + console.log('posts:', posts); + console.log('hasNextPage:', hasNextPage); + console.log('isFetching:', isFetching); + console.log('isLoading:', isLoading); return (
- setShowModal(!showModal)} size={18} color='black' /> + setShowModal(!showModal)} size={18} color="black" /> - { - showModal && ( -
-
-
- {t('notif.natification')} -
-
- close setShowModal(false)} /> -
-
- -
-
setActiveTab('all')} className={clx( - 'flex-1 border-l cursor-pointer text-white border-white border-opacity-70 flex justify-center items-center', - activeTab === 'all' ? 'bg-white bg-opacity-70 text-black' : '' - )}> - همه -
-
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' : '' - )}> - خوانده شده -
-
- -
-
-
- -
-
-
- لورم ایپسوم متن ساختگی با تولید سادگی -
-
-
۲ ساعت پیش
-
- -
دیزاین
-
-
-
-
-
-
- -
-
-
- لورم ایپسوم متن ساختگی با تولید سادگی -
-
-
۲ ساعت پیش
-
- -
دیزاین
-
-
-
-
-
-
- -
-
-
- لورم ایپسوم متن ساختگی با تولید سادگی -
-
-
۲ ساعت پیش
-
- -
دیزاین
-
-
-
-
+ {showModal && ( +
+
+
{t('notif.natification')}
+
+ close setShowModal(false)} />
- ) - } -
- ) -} -export default Notifications \ No newline at end of file +
+
setActiveTab('all')} + className={clx( + 'flex-1 border-l cursor-pointer text-white border-white border-opacity-70 flex justify-center items-center', + activeTab === 'all' ? 'bg-white bg-opacity-70 text-black' : '' + )} + > + همه +
+
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' : '' + )} + > + خوانده شده +
+
+ +
+ در حال بارگذاری...} + scrollThreshold={0.9} + // scrollableTarget="notificationsContainer" + pullDownToRefresh + + + + + > + {isLoading ? ( +

در حال بارگذاری...

+ ) : isFetching ? ( +

در حال بارگذاری صفحه بعدی...

+ ) : ( + posts.map((item: NotificationItemType) => ( +
+
+ +
+
+
{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..2b0482f --- /dev/null +++ b/src/pages/notification/hooks/useNotificationData.ts @@ -0,0 +1,17 @@ +import * as api from "../service/NotificationService"; +import { useInfiniteQuery } from "@tanstack/react-query"; + +export const useGetNotification = () => { + return useInfiniteQuery({ + queryKey: ["notifications"], + queryFn: ({ pageParam = 1 }) => api.getNotifications(pageParam), // فراخوانی API برای گرفتن داده‌ها + initialPageParam: 1, // صفحه اول به طور پیشفرض + getNextPageParam: (lastPage) => { + const currentPage = lastPage.data?.pager.page; + const totalPages = lastPage.data?.pager.totalPages; + console.log("hasNextPage check: ", currentPage, totalPages); // بررسی وضعیت صفحات + + return currentPage < totalPages ? currentPage + 1 : undefined; + }, + }); +}; diff --git a/src/pages/notification/service/NotificationService.ts b/src/pages/notification/service/NotificationService.ts new file mode 100644 index 0000000..a6b1388 --- /dev/null +++ b/src/pages/notification/service/NotificationService.ts @@ -0,0 +1,6 @@ +import axios from "../../../config/axios"; + +export const getNotifications = async (page: number) => { + const { data } = await axios.get(`/notifications?page=${page}`); + return data; +}; diff --git a/src/pages/notification/types/NotificationTypes.ts b/src/pages/notification/types/NotificationTypes.ts new file mode 100644 index 0000000..c91a804 --- /dev/null +++ b/src/pages/notification/types/NotificationTypes.ts @@ -0,0 +1,7 @@ +export type NotificationItemType = { + id: string; + title: string; + message: string; + createdAt: string; + isRead: boolean; +};