diff --git a/.env b/.env new file mode 100644 index 0000000..36649b0 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +VITE_TOKEN_NAME = 'dsc_token' +VITE_REFRESH_TOKEN_NAME = 'dsc_refresh_token' +VITE_DANAK_BASE_URL ='https://api.danakcorp.com' +VITE_BASE_URL = 'http://192.168.1.134:4000' +VITE_BASE_URL = 'https://dmail-api.danakcorp.com' +VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e' diff --git a/orval.config.ts b/orval.config.ts deleted file mode 100644 index 37d8a70..0000000 --- a/orval.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default { - dmail: { - input: "./openapi.yaml", // مسیر فایل OpenAPI - output: { - mode: "tags-split", // یا 'single' - target: "./src/api", // خروجی فایل‌های API - client: "react-query", // یا 'axios' - schemas: "./src/api/model", // خروجی مدل‌ها - }, - }, -}; diff --git a/package.json b/package.json index 6c10159..02554d5 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,15 @@ "@react-three/drei": "^10.1.2", "@react-three/fiber": "^9.1.2", "@tailwindcss/vite": "^4.1.4", + "@tanstack/react-query": "^5.81.5", + "axios": "^1.10.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "i18next": "^25.0.1", "iconsax-react": "^0.0.8", + "js-cookie": "^3.0.5", "lucide-react": "^0.511.0", + "moment-jalaali": "^0.10.4", "quill": "^2.0.3", "react": "^19.0.0", "react-colorful": "^5.6.1", @@ -44,6 +48,8 @@ }, "devDependencies": { "@eslint/js": "^9.22.0", + "@types/js-cookie": "^3.0.6", + "@types/moment-jalaali": "^0.7.9", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@types/three": "^0.177.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cf8736..87cd23d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,12 @@ importers: '@tailwindcss/vite': specifier: ^4.1.4 version: 4.1.7(vite@6.3.5(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)) + '@tanstack/react-query': + specifier: ^5.81.5 + version: 5.81.5(react@19.1.0) + axios: + specifier: ^1.10.0 + version: 1.10.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -35,9 +41,15 @@ importers: iconsax-react: specifier: ^0.0.8 version: 0.0.8(react@19.1.0) + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 lucide-react: specifier: ^0.511.0 version: 0.511.0(react@19.1.0) + moment-jalaali: + specifier: ^0.10.4 + version: 0.10.4 quill: specifier: ^2.0.3 version: 2.0.3 @@ -102,6 +114,12 @@ importers: '@eslint/js': specifier: ^9.22.0 version: 9.27.0 + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/moment-jalaali': + specifier: ^0.7.9 + version: 0.7.9 '@types/react': specifier: ^19.0.10 version: 19.1.6 @@ -1012,6 +1030,14 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 + '@tanstack/query-core@5.81.5': + resolution: {integrity: sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q==} + + '@tanstack/react-query@5.81.5': + resolution: {integrity: sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw==} + peerDependencies: + react: ^18 || ^19 + '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} @@ -1039,9 +1065,15 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/moment-jalaali@0.7.9': + resolution: {integrity: sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==} + '@types/node@24.0.7': resolution: {integrity: sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==} @@ -1219,6 +1251,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + attr-accept@2.2.5: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} @@ -1227,6 +1262,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1319,6 +1357,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -1380,6 +1422,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} @@ -1602,10 +1648,23 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + engines: {node: '>= 6'} + fs-extra@11.3.0: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} @@ -1897,10 +1956,17 @@ packages: peerDependencies: react: ^19.0.0 + jalaali-js@1.2.8: + resolution: {integrity: sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==} + jiti@2.4.2: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2132,6 +2198,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2160,6 +2234,15 @@ packages: engines: {node: '>=10'} hasBin: true + moment-jalaali@0.10.4: + resolution: {integrity: sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==} + + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2325,6 +2408,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -3945,6 +4031,13 @@ snapshots: tailwindcss: 4.1.7 vite: 6.3.5(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0) + '@tanstack/query-core@5.81.5': {} + + '@tanstack/react-query@5.81.5(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.81.5 + react: 19.1.0 + '@tweenjs/tween.js@23.1.3': {} '@types/babel__core@7.20.5': @@ -3980,8 +4073,14 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/js-cookie@3.0.6': {} + '@types/json-schema@7.0.15': {} + '@types/moment-jalaali@0.7.9': + dependencies: + moment: 2.30.1 + '@types/node@24.0.7': dependencies: undici-types: 7.8.0 @@ -4187,12 +4286,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + attr-accept@2.2.5: {} available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 + axios@1.10.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.3 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -4286,6 +4395,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + compare-versions@6.1.1: {} concat-map@0.0.1: {} @@ -4351,6 +4464,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + dependency-graph@0.11.0: {} detect-gpu@5.0.70: @@ -4667,10 +4782,20 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 + form-data@4.0.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fs-extra@11.3.0: dependencies: graceful-fs: 4.2.11 @@ -4952,8 +5077,12 @@ snapshots: transitivePeerDependencies: - '@types/react' + jalaali-js@1.2.8: {} + jiti@2.4.2: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -5137,6 +5266,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-fn@2.1.0: {} minimatch@3.1.2: @@ -5159,6 +5294,18 @@ snapshots: mkdirp@3.0.1: {} + moment-jalaali@0.10.4: + dependencies: + jalaali-js: 1.2.8 + moment: 2.30.1 + moment-timezone: 0.5.48 + + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -5359,6 +5506,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.1.0: {} + punycode.js@2.3.1: {} punycode@2.3.1: {} diff --git a/src/App.tsx b/src/App.tsx index 6cd3b6b..db55bd4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,9 @@ import i18next from 'i18next' import FaJson from './langs/fa.json' import { I18nextProvider } from 'react-i18next' import NewMessage from './components/newMessage/NewMessage'; +import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { IApiErrorRepsonse } from './types/error.types'; +import ToastContainer from './components/Toast'; i18next.init({ interpolation: { escapeValue: false }, lng: 'fa', @@ -20,12 +23,34 @@ i18next.init({ } }) +const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: async (error: IApiErrorRepsonse) => { + if (error?.response?.statusCode === 401) { + console.log(401); + + } + } + }), + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: false + // staleTime: 86400000 // 1 day in milliseconds + } + } +}); + const App: FC = () => { return ( -
- + + +
+ + + ) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 677e97d..4dd4cf6 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,5 +1,7 @@ import { FC, ReactNode } from 'react' import { clx } from '../helpers/utils' +import MoonLoader from "react-spinners/ClipLoader" + interface ButtonProps { label?: string; @@ -28,10 +30,8 @@ const Button: FC = (props) => { className={buttonClass} > {props.loading ? ( -
-
- درحال بارگذاری... -
+ + ) : ( props.children || props.label )} diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..fe41eb7 --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,72 @@ +import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react'; +import React, { useState, useEffect } from 'react'; + +interface Toast { + id: string; + message: string; + type?: 'success' | 'error' | 'info'; + isExiting?: boolean; +} + +let addToast: (toast: Toast) => void; + +const ToastContainer: React.FC = () => { + const [toasts, setToasts] = useState([]); + + // ثبت toast جدید + const showToast = (toast: Toast) => { + setToasts((prev) => [...prev, toast]); + + // حذف خودکار بعد از ۳ ثانیه + setTimeout(() => { + setToasts((prev) => prev.map(t => + t.id === toast.id ? { ...t, isExiting: true } : t + )); + + // حذف از DOM بعد از اتمام انیمیشن + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== toast.id)); + }, 300); // مدت زمان انیمیشن خروج + }, 3000); + }; + + // تخصیص تابع نمایش toast به متغیر سراسری + useEffect(() => { + addToast = showToast; + }, []); + + return ( +
+ {toasts.map((toast) => ( +
+ { + toast.type === 'success' ? : + toast.type === 'error' ? : + + } + {toast.message} +
+ ))} +
+ ); +}; + +export const toast = (message?: string, type: 'success' | 'error' | 'info' = 'info') => { + addToast({ id: Date.now().toString(), message: message || '', type }); +}; + +export default ToastContainer; \ No newline at end of file diff --git a/src/config/axios.ts b/src/config/axios.ts new file mode 100644 index 0000000..2af51c6 --- /dev/null +++ b/src/config/axios.ts @@ -0,0 +1,25 @@ +import axios from "axios"; +import { getToken } from "./func"; + +const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_BASE_URL, +}); + +axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + return Promise.reject(error); + } +); + +axiosInstance.interceptors.request.use(async function (config) { + const token = getToken(); + + config.headers.Authorization = "Bearer " + token; + config.headers["x-business-id"] = localStorage.getItem( + import.meta.env.VITE_WORKSPACE_ID + ); + return config; +}); + +export default axiosInstance; diff --git a/src/config/axiosDanak.ts b/src/config/axiosDanak.ts new file mode 100644 index 0000000..580452f --- /dev/null +++ b/src/config/axiosDanak.ts @@ -0,0 +1,22 @@ +import axios from "axios"; +import { getToken } from "./func"; + +const axiosDanak = axios.create({ + baseURL: import.meta.env.VITE_DANAK_BASE_URL, +}); + +axiosDanak.interceptors.response.use( + (response) => response, + (error) => { + return Promise.reject(error); + } +); + +axiosDanak.interceptors.request.use(async function (config) { + const token = getToken(); + + config.headers.Authorization = "Bearer " + token; + return config; +}); + +export default axiosDanak; diff --git a/src/config/func.ts b/src/config/func.ts new file mode 100644 index 0000000..1dc7af7 --- /dev/null +++ b/src/config/func.ts @@ -0,0 +1,61 @@ +import Cookie from "js-cookie"; + +export function isEmail(input: string): boolean { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(input); +} + +export function NumberFormat(number: number): string { + return Intl.NumberFormat("fa-IR").format(number); +} + +export const getToken = () => { + return Cookie.get(import.meta.env.VITE_TOKEN_NAME); +}; + +export const getRefreshToken = () => { + return Cookie.get(import.meta.env.VITE_REFRESH_TOKEN_NAME); +}; + +const isProduction = import.meta.env.MODE === "production"; +const domain = isProduction ? ".danakcorp.com" : undefined; + +export const setToken = async (token: string) => { + Cookie.set(import.meta.env.VITE_TOKEN_NAME, token, { domain }); +}; + +export const setRefreshToken = async (refreshToken: string) => { + Cookie.set(import.meta.env.VITE_REFRESH_TOKEN_NAME, refreshToken, { + domain, + }); +}; + +export const removeToken = async () => { + Cookie.remove(import.meta.env.VITE_TOKEN_NAME, { domain, path: "/" }); +}; + +export const removeRefreshToken = async () => { + Cookie.remove(import.meta.env.VITE_REFRESH_TOKEN_NAME, { domain, path: "/" }); +}; + +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/config/orvalAxios.ts b/src/config/orvalAxios.ts new file mode 100644 index 0000000..22ea517 --- /dev/null +++ b/src/config/orvalAxios.ts @@ -0,0 +1,50 @@ +import axios, { AxiosRequestConfig } from "axios"; +import { getToken } from "./func"; + +const orvalAxiosInstance = axios.create({ + baseURL: import.meta.env.VITE_BASE_URL, +}); + +orvalAxiosInstance.interceptors.response.use( + (response) => response, + (error) => { + return Promise.reject(error); + } +); + +orvalAxiosInstance.interceptors.request.use(async (config) => { + const token = getToken(); + + config.headers["Content-Type"] = "application/json"; + config.headers["Accept"] = "application/json"; + + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + } + + const businessId = localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID); + if (businessId) { + config.headers["x-business-id"] = businessId; + } + + return config; +}); + +export const customAxiosInstance = ( + config: AxiosRequestConfig, + options?: AxiosRequestConfig +): Promise => { + const source = axios.CancelToken.source(); + const promise = orvalAxiosInstance({ + ...config, + ...options, + cancelToken: source.token, + }).then(({ data }) => data); + + // @ts-expect-error - Adding cancel method to promise + promise.cancel = () => { + source.cancel("Query was cancelled"); + }; + + return promise; +}; diff --git a/src/langs/fa.json b/src/langs/fa.json index 7def5d9..f7b5adb 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -118,6 +118,21 @@ "description_sign": "توضیحات امضا", "select_section": "لطفا یک بخش انتخاب کنید" }, + "domain": { + "table": { + "record_type": "نوع رکورد", + "record_name": "نام رکورد", + "record_value": "مقدار رکورد", + "priority": "اولویت", + "ttl": "TTL", + "status": "وضعیت" + }, + "status": { + "verified": "تایید شده", + "pending": "در انتظار", + "failed": "ناموفق" + } + }, "select_file": "انتخاب فایل", "upload": "آپلود", "new_message": { diff --git a/src/locales/fa.json b/src/locales/fa.json deleted file mode 100644 index aadf73b..0000000 --- a/src/locales/fa.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "sidebar": { - "menu": "منو", - "received": "دریافتی‌ها", - "sent": "ارسال شده‌ها", - "draft": "پیش‌نویس‌ها", - "archive": "آرشیو", - "trash": "سطل زباله", - "other": "سایر", - "favorite": "مورد علاقه", - "spam": "اسپم", - "setting": "تنظیمات", - "logout": "خروج", - "new_message": "پیام جدید" - }, - "new_message": { - "title": "پیام جدید", - "to": "به:", - "recipients_placeholder": "گیرندگان را وارد کنید", - "subject": "موضوع:", - "subject_placeholder": "موضوع پیام را وارد کنید", - "message_placeholder": "متن پیام خود را بنویسید...", - "send": "ارسال" - } -} diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index 14f7917..23630ea 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -1,32 +1,79 @@ -import { FC } from 'react' +import { FC, useEffect, useState } from 'react' +import { useWorkspaces } from './hooks/useHomeData' +import { workspaceItem } from './types/HomeTypes' +import moment from 'moment-jalaali' const Home: FC = () => { + + const getWorkspaces = useWorkspaces() + const [selectedWorkspace, setSelectedWorkspace] = useState(null) + + useEffect(() => { + setSelectedWorkspace(localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID)) + }, []) + + useEffect(() => { + if (getWorkspaces.isError && getWorkspaces.error instanceof Error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error = getWorkspaces.error as any; + if (error.response?.status === 403) { + localStorage.removeItem(import.meta.env.VITE_WORKSPACE_ID) + window.location.href = 'https://console.danakcorp.com' + } + } + }, [getWorkspaces.isError, getWorkspaces.data]) + + + const handleChangeWorkspace = (id: string) => { + localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, id) + setSelectedWorkspace(id) + window.location.reload() + } + return ( -
-
-

- خوش آمدید به سیستم مدیریت ایمیل -

-

- از منوی کناری می‌توانید به بخش‌های مختلف سیستم دسترسی داشته باشید -

-
-
-

پیام‌های دریافتی

-

مدیریت ایمیل‌های واردشده

-
-
-

پیام‌های ارسالی

-

مشاهده ایمیل‌های ارسال شده

-
-
-

پیش‌نویس‌ها

-

ادامه نوشتن ایمیل‌های ناتمام

-
-
-

تنظیمات

-

پیکربندی سیستم و حساب کاربری

-
+
+
+ +
+ { + getWorkspaces.data?.data?.workspaces?.map((item: workspaceItem) => { + return ( +
handleChangeWorkspace(item.id)} + > +
+ {item.businessName} +
+ +
+ {item.plan?.name} +
+ +
+
+
تاریخ پایان:
+
{moment(item?.endDate).format('jYYYY/jMM/jDD')}
+
+ + +
+ + + + +
+ ) + }) + } + +
diff --git a/src/pages/home/components/BoxNewAccessbility.tsx b/src/pages/home/components/BoxNewAccessbility.tsx new file mode 100644 index 0000000..d98efeb --- /dev/null +++ b/src/pages/home/components/BoxNewAccessbility.tsx @@ -0,0 +1,146 @@ +import { Add } from 'iconsax-react' +import { FC, useState } from 'react' +import DefaulModal from '../../../components/DefaulModal' +import { useTranslation } from 'react-i18next' +import Input from '../../../components/Input' +import Select from '../../../components/Select' +import SwitchComponent from '../../../components/Switch' + +const BoxNewAccessbility: FC = () => { + + const [open, setOpen] = useState(false) + const { t } = useTranslation('global') + + return ( + <> +
setOpen(true)} + className="flex-1 min-w-[40%] xl:min-w-[20%] flex justify-center items-center h-[160px]" + style={{ + position: 'relative', + }} + > + + + +
+ +
+ + +
+ + setOpen(false)} + isHeader={true} + title_header={t('home.create_new_access')} + > +
+
+
+ +
+ + - -
-
- +
- - ) } diff --git a/src/pages/setting/domain/components/CreateDomain.tsx b/src/pages/setting/domain/components/CreateDomain.tsx new file mode 100644 index 0000000..1721bd9 --- /dev/null +++ b/src/pages/setting/domain/components/CreateDomain.tsx @@ -0,0 +1,76 @@ +import Button from '@/components/Button' +import Input from '@/components/Input' +import { toast } from '@/components/Toast' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useCreateDomain, useGetDomains } from '../hooks/useDomainData' +import { ErrorType } from '@/helpers/types' + +const CreateDomain: FC = () => { + + const { t } = useTranslation() + const { mutate: createDomain, isPending } = useCreateDomain() + const { data: domains } = useGetDomains() + const [domain, setDomain] = useState('') + const [isVerified, setIsVerified] = useState(false) + + useEffect(() => { + if (domains?.data?.domain) { + setDomain(domains?.data?.domain?.name) + if (domains?.data?.domain?.isVerified) { + setIsVerified(true) + } + } + }, [domains]) + + + const handleCreateDomain = () => { + createDomain({ + name: domain, + notes: domain + }, { + onSuccess: (data) => { + toast(data.message, 'success') + setDomain('') + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error?.message[0], 'error') + } + }) + } + return ( +
+
+
+ {t('setting.record_dns')} +
+

+ {t('setting.record_dns_description')} +

+
+ +
+ setDomain(e.target.value)} + readOnly={isVerified} + /> + + { + !isVerified && +
+
+ ) +} + +export default CreateDomain \ No newline at end of file diff --git a/src/pages/setting/domain/hooks/useDomainData.ts b/src/pages/setting/domain/hooks/useDomainData.ts new file mode 100644 index 0000000..f807910 --- /dev/null +++ b/src/pages/setting/domain/hooks/useDomainData.ts @@ -0,0 +1,32 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { CreateDomainType } from "../types/Types"; +import * as api from "../service/DomainService"; + +export const useCreateDomain = () => { + return useMutation({ + mutationFn: (variables: CreateDomainType) => api.createDomain(variables), + }); +}; + +export const useGetDomains = () => { + return useQuery({ + queryKey: ["domains"], + queryFn: () => api.getDomains(), + }); +}; + +export const useGetDnsRecords = (disabled?: boolean) => { + return useQuery({ + queryKey: ["dns-records"], + queryFn: () => api.getDnsRecords(), + refetchInterval: 3000, + enabled: !disabled, + }); +}; + +export const useVerifyDnsRecord = () => { + return useQuery({ + queryKey: ["verify-dns-record"], + queryFn: () => api.verifyDnsRecord(), + }); +}; diff --git a/src/pages/setting/domain/service/DomainService.ts b/src/pages/setting/domain/service/DomainService.ts new file mode 100644 index 0000000..990f073 --- /dev/null +++ b/src/pages/setting/domain/service/DomainService.ts @@ -0,0 +1,22 @@ +import axios from "@/config/axios"; +import { CreateDomainType } from "../types/Types"; + +export const createDomain = async (params: CreateDomainType) => { + const { data } = await axios.post(`/domains`, params); + return data; +}; + +export const getDomains = async () => { + const { data } = await axios.get(`/domains`); + return data; +}; + +export const getDnsRecords = async () => { + const { data } = await axios.get(`/domains/dns-records`); + return data; +}; + +export const verifyDnsRecord = async () => { + const { data } = await axios.get(`/domains/verify-dns`); + return data; +}; diff --git a/src/pages/setting/domain/types/Types.ts b/src/pages/setting/domain/types/Types.ts new file mode 100644 index 0000000..33ed777 --- /dev/null +++ b/src/pages/setting/domain/types/Types.ts @@ -0,0 +1,33 @@ +import { IResponse } from "@/types/response.types"; + +export type CreateDomainType = { + name: string; + notes?: string; +}; + +export type DnsRecordType = { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + wildduckId: string | null; + name: string; + type: "MX" | "SPF" | "DMARC" | "DKIM" | "TXT" | "A" | "AAAA" | "CNAME"; + value: string; + ttl: number; + priority: number | null; + status: "PENDING" | "VERIFIED" | "FAILED"; + isRequired: boolean; + isActive: boolean; + lastVerifiedAt: string | null; + lastCheckedAt: string | null; + description: string; + errorMessage: string | null; + verificationAttempts: number; + nextVerificationAt: string | null; + domain: string; +}; + +export type DnsRecordResponseType = IResponse<{ + dnsRecords: DnsRecordType[]; +}>; diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 77c46da..05409d3 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -8,10 +8,11 @@ import DraftList from '@/pages/draft/List' import ArchiveList from '@/pages/archive/List' import TrashList from '@/pages/Trash/List' import DetailEmail from '@/pages/received/Detail' +import Home from '@/pages/home/Home' const AppRouter: FC = () => { return ( - } /> + } /> } /> } /> } /> diff --git a/src/types/error.types.ts b/src/types/error.types.ts new file mode 100644 index 0000000..facf569 --- /dev/null +++ b/src/types/error.types.ts @@ -0,0 +1,14 @@ +import { DefaultError } from "@tanstack/react-query"; +import { IBaseResponse } from "./response.types"; + +interface IError { + message: string[]; +} + +export interface IErrorResponse extends IBaseResponse { + error: IError; +} + +export interface IApiErrorRepsonse extends DefaultError { + response?: IErrorResponse; +} diff --git a/src/types/response.types.ts b/src/types/response.types.ts new file mode 100644 index 0000000..b427094 --- /dev/null +++ b/src/types/response.types.ts @@ -0,0 +1,7 @@ +export interface IBaseResponse { + statusCode: number; + success: boolean; +} +export interface IResponse extends IBaseResponse { + data: T; +}