diff --git a/.env b/.env index 24d57e5..a5473ac 100644 --- a/.env +++ b/.env @@ -1,6 +1,13 @@ VITE_TOKEN_NAME = 'dmail_token' VITE_REFRESH_TOKEN_NAME = 'dmail_refresh_token' + VITE_DANAK_BASE_URL ='https://api.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.1.118:4000' + +VITE_SOCKET_URL = 'wss://dmail-api.danakcorp.com/email' +# VITE_SOCKET_URL = 'ws://192.168.1.118:4000/email' + VITE_BASE_URL = 'https://dmail-api.danakcorp.com' +# VITE_BASE_URL = 'http://192.168.1.118:4000' + + VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e' diff --git a/package.json b/package.json index 746eae4..2668b14 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "orval": "orval" }, "dependencies": { + "@headlessui/react": "^2.2.4", "@hookform/resolvers": "^5.1.1", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-switch": "^1.2.5", @@ -35,6 +36,7 @@ "react-dropzone": "^14.3.8", "react-hook-form": "^7.60.0", "react-i18next": "^15.5.1", + "react-infinite-scroll-component": "^6.1.0", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27545cb..e6f5635 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@headlessui/react': + specifier: ^2.2.4 + version: 2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@hookform/resolvers': specifier: ^5.1.1 version: 5.1.1(react-hook-form@7.60.0(react@19.1.0)) @@ -80,6 +83,9 @@ importers: react-i18next: specifier: ^15.5.1 version: 15.5.2(i18next@25.2.1(typescript@5.7.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.7.3) + react-infinite-scroll-component: + specifier: ^6.1.0 + version: 6.1.0(react@19.1.0) react-loading-skeleton: specifier: ^3.5.0 version: 3.5.0(react@19.1.0) @@ -480,9 +486,37 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + + '@floating-ui/react-dom@2.1.4': + resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@gerrit0/mini-shiki@3.7.0': resolution: {integrity: sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==} + '@headlessui/react@2.2.4': + resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + '@hookform/resolvers@5.1.1': resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} peerDependencies: @@ -736,6 +770,38 @@ packages: '@types/react': optional: true + '@react-aria/focus@3.20.5': + resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.25.3': + resolution: {integrity: sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.9': + resolution: {integrity: sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.29.1': + resolution: {integrity: sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/flags@3.1.2': + resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} + + '@react-stately/utils@3.10.7': + resolution: {integrity: sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@react-three/drei@10.1.2': resolution: {integrity: sha512-CCcLAqZEvYiUErOcJgGzovY3RH6KgdrqD4ubeAx1nyGbSPLnKR9a8ynYbPdtZhIyiWqGc07z+RYQzpaOfN4ZIA==} peerDependencies: @@ -772,6 +838,11 @@ packages: react-native: optional: true + '@react-types/shared@3.30.0': + resolution: {integrity: sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@rolldown/pluginutils@1.0.0-beta.9': resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} @@ -969,6 +1040,9 @@ packages: resolution: {integrity: sha512-JZlVFE6/dYpP9tQmV0/ADfn32L9uFarHWxfcRhReKUnljz1ZiUM5zpX+PH8h5CJs6lao3TuFqnPm9IJJCEkE2w==} engines: {node: '>=10.8'} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@tailwindcss/node@4.1.7': resolution: {integrity: sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==} @@ -1067,6 +1141,15 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} @@ -2551,6 +2634,11 @@ packages: typescript: optional: true + react-infinite-scroll-component@6.1.0: + resolution: {integrity: sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==} + peerDependencies: + react: '>=16.0.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2841,6 +2929,9 @@ packages: resolution: {integrity: sha512-S5FVf6zWynPWooi7pJ7lZhSUe2snTzqLuUzbd5h5PHUOhzgvW0bLKBd2wv0ixn6/5o9vwc/IkQT74CRcLJQzeg==} engines: {node: '>= 4.7.0'} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwind-merge@3.3.0: resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} @@ -2868,6 +2959,10 @@ packages: three@0.177.0: resolution: {integrity: sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==} + throttle-debounce@2.3.0: + resolution: {integrity: sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==} + engines: {node: '>=8'} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -3475,6 +3570,31 @@ snapshots: '@exodus/schemasafe@1.3.0': {} + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.2': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/react@0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@floating-ui/utils': 0.2.10 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.10': {} + '@gerrit0/mini-shiki@3.7.0': dependencies: '@shikijs/engine-oniguruma': 3.7.0 @@ -3483,6 +3603,16 @@ snapshots: '@shikijs/types': 3.7.0 '@shikijs/vscode-textmate': 10.0.2 + '@headlessui/react@2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-virtual': 3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) + '@hookform/resolvers@5.1.1(react-hook-form@7.60.0(react@19.1.0))': dependencies: '@standard-schema/utils': 0.3.0 @@ -3783,6 +3913,51 @@ snapshots: optionalDependencies: '@types/react': 19.1.6 + '@react-aria/focus@3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@react-aria/interactions@3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-aria/ssr': 3.9.9(react@19.1.0) + '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-stately/flags': 3.1.2 + '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.17 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@react-aria/ssr@3.9.9(react@19.1.0)': + dependencies: + '@swc/helpers': 0.5.17 + react: 19.1.0 + + '@react-aria/utils@3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-aria/ssr': 3.9.9(react@19.1.0) + '@react-stately/flags': 3.1.2 + '@react-stately/utils': 3.10.7(react@19.1.0) + '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@react-stately/flags@3.1.2': + dependencies: + '@swc/helpers': 0.5.17 + + '@react-stately/utils@3.10.7(react@19.1.0)': + dependencies: + '@swc/helpers': 0.5.17 + react: 19.1.0 + '@react-three/drei@10.1.2(@react-three/fiber@9.1.2(@types/react@19.1.6)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.177.0))(@types/react@19.1.6)(@types/three@0.177.0)(immer@9.0.21)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.177.0)': dependencies: '@babel/runtime': 7.27.3 @@ -3838,6 +4013,10 @@ snapshots: - '@types/react' - immer + '@react-types/shared@3.30.0(react@19.1.0)': + dependencies: + react: 19.1.0 + '@rolldown/pluginutils@1.0.0-beta.9': {} '@rollup/rollup-android-arm-eabi@4.41.1': @@ -4087,6 +4266,10 @@ snapshots: '@stoplight/yaml-ast-parser': 0.0.50 tslib: 2.8.1 + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + '@tailwindcss/node@4.1.7': dependencies: '@ampproject/remapping': 2.3.0 @@ -4165,6 +4348,14 @@ snapshots: '@tanstack/query-core': 5.81.5 react: 19.1.0 + '@tanstack/react-virtual@3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/virtual-core@3.13.12': {} + '@tweenjs/tween.js@23.1.3': {} '@types/babel__core@7.20.5': @@ -5752,6 +5943,11 @@ snapshots: react-dom: 19.1.0(react@19.1.0) typescript: 5.7.3 + react-infinite-scroll-component@6.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + throttle-debounce: 2.3.0 + react-is@16.13.1: {} react-loading-skeleton@3.5.0(react@19.1.0): @@ -6100,6 +6296,8 @@ snapshots: swiper@11.2.8: {} + tabbable@6.2.0: {} + tailwind-merge@3.3.0: {} tailwindcss@4.1.7: {} @@ -6131,6 +6329,8 @@ snapshots: three@0.177.0: {} + throttle-debounce@2.3.0: {} + tiny-case@1.0.3: {} tiny-warning@1.0.3: {} diff --git a/src/assets/images/avatar_image.png b/src/assets/images/avatar_image.png new file mode 100644 index 0000000..96000aa Binary files /dev/null and b/src/assets/images/avatar_image.png differ diff --git a/src/components/StatusCircle.tsx b/src/components/StatusCircle.tsx new file mode 100644 index 0000000..97fa12a --- /dev/null +++ b/src/components/StatusCircle.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react' + +type Props = { + color: string +} + +const StatusCircle: FC = (props: Props) => { + return ( +
+ +
+ ) +} + +export default StatusCircle \ No newline at end of file diff --git a/src/hooks/useEmailWebSocket.ts b/src/hooks/useEmailWebSocket.ts index 7f75896..d5a8ea5 100644 --- a/src/hooks/useEmailWebSocket.ts +++ b/src/hooks/useEmailWebSocket.ts @@ -8,10 +8,9 @@ interface UseEmailWebSocketOptions { onDisconnect?: () => void; onError?: (error: string) => void; } - export const useEmailWebSocket = ({ token, - serverUrl = `ws://${import.meta.env.VITE_BASE_URL}/email`, + serverUrl = import.meta.env.VITE_SOCKET_URL, onConnect, onDisconnect, onError, diff --git a/src/index.css b/src/index.css index 6d306d6..01a6e8d 100644 --- a/src/index.css +++ b/src/index.css @@ -341,3 +341,10 @@ textarea::placeholder { /* حفظ padding های جدول */ padding: 8px; } + +.modalGlass3 { + background: rgba(62, 61, 61, 0.2); + background-blend-mode: multiply; + + backdrop-filter: blur(44px); +} diff --git a/src/langs/fa.json b/src/langs/fa.json index 5d7804a..21522e9 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -15,7 +15,9 @@ "new_message": "پیام جدید" }, "header": { - "search": "جستجو" + "search": "جستجو", + "profile": "پروفایل", + "setting": "تنظیمات" }, "errors": { "required": "این فیلد اجباری است", @@ -63,6 +65,10 @@ "unread": "خوانده نشده", "search": "جستجو" }, + "notif": { + "natification": "اعلان ها", + "all_read": "خواندن همه" + }, "setting": { "title": "تنظیمات", "personality": "شخصی سازی", diff --git a/src/pages/notification/Notification.tsx b/src/pages/notification/Notification.tsx new file mode 100644 index 0000000..4c0be16 --- /dev/null +++ b/src/pages/notification/Notification.tsx @@ -0,0 +1,172 @@ +import { CloseCircle, Notification } from 'iconsax-react'; +import { FC, useState } from 'react'; +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 { NotificationItemType, NotificationTypeEnum } 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 '../../components/Toast'; +// import { useNavigate } from 'react-router-dom'; +import { ErrorType } from '../../helpers/types'; +const Notifications: FC = () => { + // const navigate = useNavigate() + const { t } = useTranslation(); + 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 handleAllRead = () => { + readAll.mutate(undefined, { + onSuccess: () => { + refetch() + toast(t('success'), 'success') + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error?.message[0], 'error') + } + }) + } + + const handleRedirect = (type: NotificationTypeEnum) => { + 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')}
+
+ 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/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/70 text-black' : '' + )} + > + خوانده شده +
+
+ +
+ +
+ +
+
} + scrollableTarget="notificationsContainer" + > + { + posts.map((item: NotificationItemType) => ( +
handleRedirect(item.type)} 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..b5cac39 --- /dev/null +++ b/src/pages/notification/hooks/useNotificationData.ts @@ -0,0 +1,22 @@ +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; + + 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..8427851 --- /dev/null +++ b/src/pages/notification/service/NotificationService.ts @@ -0,0 +1,18 @@ +import axios from "../../../config/axios"; + +export const getNotifications = async ( + page: number, + status: "all" | "read" | "unread" +) => { + let query = ``; + if (status !== "all") { + query = `&isRead=${status === "read" ? 1 : 0}`; + } + const { data } = await axios.get(`/notifications?page=${page}${query}`); + return data; +}; + +export const readAll = async () => { + const { data } = await axios.patch(`/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..4985ece --- /dev/null +++ b/src/pages/notification/types/NotificationTypes.ts @@ -0,0 +1,31 @@ +export type NotificationItemType = { + id: string; + title: string; + message: string; + createdAt: string; + isRead: boolean; + type: NotificationTypeEnum; +}; + +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/pages/profile/hooks/useProfileData.ts b/src/pages/profile/hooks/useProfileData.ts new file mode 100644 index 0000000..84db9a7 --- /dev/null +++ b/src/pages/profile/hooks/useProfileData.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import * as api from "../service/Service"; + +export const useGetProfile = () => { + return useQuery({ + queryKey: ["profile"], + queryFn: api.getProfile, + }); +}; diff --git a/src/pages/profile/service/Service.ts b/src/pages/profile/service/Service.ts new file mode 100644 index 0000000..e723fa3 --- /dev/null +++ b/src/pages/profile/service/Service.ts @@ -0,0 +1,6 @@ +import axios from "@/config/axios"; + +export const getProfile = async () => { + const { data } = await axios.get("/users/me"); + return data; +}; diff --git a/src/shared/Header.tsx b/src/shared/Header.tsx index 545bbe1..612a8cb 100644 --- a/src/shared/Header.tsx +++ b/src/shared/Header.tsx @@ -1,73 +1,184 @@ import { FC, useEffect, useState } from 'react' import Input from '../components/Input' -import { Element3, HambergerMenu, Wallet } from 'iconsax-react' +import { ArrowDown2, CloseCircle, HambergerMenu, Logout, ProfileCircle, SearchNormal, Setting2 } from 'iconsax-react' import { useTranslation } from 'react-i18next' -import { Link, useLocation, useNavigate } from 'react-router-dom' -import { Paths } from '@/utils/Paths' -import { EmailNotifications } from '@/components/EmailNotifications' +import Notifications from '../pages/notification/Notification' import { useSharedStore } from './store/sharedStore' -// import { useGetProfile } from '../pages/profile/hooks/useProfileData' -// import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' -// import { useGetWalletBalance } from '../pages/wallet/hooks/useWalletData' -// import { NumberFormat } from '../config/func' +import { useGetProfile } from '../pages/profile/hooks/useProfileData' +import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' +import { Link } from 'react-router-dom' +import { Paths } from '@/utils/Paths' +import SideBarItem from './SideBarItem' +import AvatarImage from '../assets/images/avatar_image.png' const Header: FC = () => { - const location = useLocation(); - const navigate = useNavigate(); const [popoverKey, setPopoverKey] = useState(0); + const { data } = useGetProfile() + const [search, setSearch] = useState('') + const [showSearchResults, setShowSearchResults] = useState(false) + const [inputFocused, setInputFocused] = useState(false) + const [showMobileSearch, setShowMobileSearch] = useState(false) + const { t } = useTranslation('global') const { setOpenSidebar, openSidebar } = useSharedStore() - // const { data } = useGetProfile() - // const getWalletBalance = useGetWalletBalance() - // فرض می‌کنیم که token از localStorage گرفته می‌شود - const userToken = localStorage.getItem('jwt-token') || ''; + const handleSearchChange = (value: string) => { + setSearch(value) + if (value) { + setShowSearchResults(true) + } else { + setShowSearchResults(false) + } + } + + const handleInputFocus = () => { + setInputFocused(true) + if (search) { + setShowSearchResults(true) + } + } + + const handleInputBlur = (e: React.FocusEvent) => { + const searchContainer = document.getElementById('search-dropdown-container') + const mobileSearchContainer = document.getElementById('mobile-search-dropdown-container') + + if ((searchContainer && searchContainer.contains(e.relatedTarget as Node)) || + (mobileSearchContainer && mobileSearchContainer.contains(e.relatedTarget as Node))) { + return + } + + setInputFocused(false) + setTimeout(() => { + setShowSearchResults(false) + }, 200) + } + + const toggleMobileSearch = () => { + setShowMobileSearch(prev => !prev) + if (!showMobileSearch) { + setTimeout(() => { + const mobileSearchInput = document.getElementById('mobile-search-input') + if (mobileSearchInput) { + (mobileSearchInput as HTMLInputElement).focus() + setInputFocused(true) + } + }, 100) + } else { + setSearch('') + setShowSearchResults(false) + } + } - console.log(popoverKey); useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + const searchContainer = document.getElementById('search-dropdown-container') + const mobileSearchContainer = document.getElementById('mobile-search-dropdown-container') + const searchInput = document.getElementById('search-input-container') + const mobileSearchInput = document.getElementById('mobile-search-input-container') - setPopoverKey((prevKey) => prevKey + 1); - }, [location.pathname]); + const isClickOutsideDesktopSearch = + searchContainer && !searchContainer.contains(e.target as Node) && + searchInput && !searchInput.contains(e.target as Node); + + const isClickOutsideMobileSearch = + mobileSearchContainer && !mobileSearchContainer.contains(e.target as Node) && + mobileSearchInput && !mobileSearchInput.contains(e.target as Node); + + if ((searchContainer && searchInput && isClickOutsideDesktopSearch) || + (mobileSearchContainer && mobileSearchInput && isClickOutsideMobileSearch)) { + setShowSearchResults(false) + setInputFocused(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) return ( -
+
+ {/* Desktop Search */} +
+
+ +
-
- -
-
setOpenSidebar(!openSidebar)} className='xl:hidden block'> - -
- {/* */} -
- - - - - - - -
-
- {/* {NumberFormat(getWalletBalance.data?.data?.balance) + ' ' + t('toman')} */} -
-
- + {showSearchResults && search && inputFocused && ( +
+
+ نتیجه‌ای یافت نشد
- - {userToken && ( - navigate(`/mail/${email.messageId}`)} - /> )} - {/* { +
+ + {/* Mobile Layout */} +
+ {!showMobileSearch && ( +
setOpenSidebar(!openSidebar)} className='xl:hidden block'> + +
+ )} + + {/* Full width mobile search when active */} + {showMobileSearch && ( +
+
+ +
+
+ +
+
+ )} +
+ + {/* Mobile Search Results */} + {showMobileSearch && showSearchResults && search && ( +
+
+ نتیجه‌ای یافت نشد +
+
+ )} + +
+ {/* Mobile search icon */} + {!showMobileSearch && ( +
+ +
+ )} + + + + { data && ( @@ -77,7 +188,7 @@ const Header: FC = () => {
- {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName} + {data?.data?.user?.emailAddress}
@@ -88,13 +199,13 @@ const Header: FC = () => {
setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
- +
- {data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName} + {data?.data?.user?.emailAddress}
@@ -103,51 +214,12 @@ const Header: FC = () => {
-
- } - title={t('header.wallet')} - link={Pages.wallet} - isActive - isWithoutLine - /> -
-
{t('home.balance')}
-
- {NumberFormat(getWalletBalance.data?.data?.balance) + ' ' + t('toman')} -
-
-
- - } - title={t('sidebar.transactions')} - isActive - link={Pages.transactions} - isWithoutLine - /> - - } - title={t('header.discounts')} - isActive - link={Pages.discounts} - isWithoutLine - /> - - } - title={t('header.financial_info')} - isActive - link={Pages.financial} - isWithoutLine - /> } title={t('header.profile')} isActive - link={Pages.profile} + link={Paths.profile} isWithoutLine /> @@ -155,7 +227,7 @@ const Header: FC = () => { icon={} title={t('header.setting')} isActive - link={Pages.setting} + link={Paths.setting} isWithoutLine />
@@ -164,8 +236,8 @@ const Header: FC = () => { } title={t('sidebar.logout')} - isActive - link={Pages.setting} + isActive={false} + link={'#'} isWithoutLine isLogout /> @@ -174,7 +246,7 @@ const Header: FC = () => { ) - } */} + }
) diff --git a/src/shared/SideBarItem.tsx b/src/shared/SideBarItem.tsx index 55560f8..308bf65 100644 --- a/src/shared/SideBarItem.tsx +++ b/src/shared/SideBarItem.tsx @@ -1,6 +1,8 @@ import { FC, ReactNode } from 'react' import { Link } from 'react-router-dom' import { clx } from '../helpers/utils' +import { removeRefreshToken, removeToken } from '@/config/func' +import { Paths } from '@/utils/Paths' type Props = { @@ -14,8 +16,14 @@ type Props = { const SideBarItem: FC = (props: Props) => { + const handleLogout = () => { + removeToken() + removeRefreshToken() + window.location.href = Paths.login + } + return ( - + { !props.isWithoutLine &&