diff --git a/.env b/.env index 36649b0..fb7a36c 100644 --- a/.env +++ b/.env @@ -2,5 +2,5 @@ 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_BASE_URL = 'https://dmail-api.danakcorp.com' VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e' diff --git a/package.json b/package.json index 02554d5..92f1890 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "orval": "orval" }, "dependencies": { + "@hookform/resolvers": "^5.1.1", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-switch": "^1.2.5", "@react-three/drei": "^10.1.2", @@ -31,6 +32,7 @@ "react-date-object": "^2.1.9", "react-dom": "^19.0.0", "react-dropzone": "^14.3.8", + "react-hook-form": "^7.60.0", "react-i18next": "^15.5.1", "react-loading-skeleton": "^3.5.0", "react-multi-date-picker": "^4.5.2", @@ -44,6 +46,7 @@ "three": "^0.177.0", "uuid": "^11.1.0", "vite-tsconfig-paths": "^5.1.4", + "yup": "^1.6.1", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87cd23d..6530d44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^5.1.1 + version: 5.1.1(react-hook-form@7.60.0(react@19.1.0)) '@radix-ui/react-checkbox': specifier: ^1.3.2 version: 1.3.2(@types/react-dom@19.1.5(@types/react@19.1.6))(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -68,6 +71,9 @@ importers: react-dropzone: specifier: ^14.3.8 version: 14.3.8(react@19.1.0) + react-hook-form: + specifier: ^7.60.0 + version: 7.60.0(react@19.1.0) 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) @@ -107,6 +113,9 @@ importers: vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@24.0.7)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0)) + yup: + specifier: ^1.6.1 + version: 1.6.1 zustand: specifier: ^5.0.3 version: 5.0.5(@types/react@19.1.6)(immer@9.0.21)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) @@ -465,6 +474,11 @@ packages: '@gerrit0/mini-shiki@3.7.0': resolution: {integrity: sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==} + '@hookform/resolvers@5.1.1': + resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -867,6 +881,9 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@stoplight/better-ajv-errors@1.0.3': resolution: {integrity: sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA==} engines: {node: ^12.20 || >= 14.13} @@ -2408,6 +2425,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -2463,6 +2483,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + react-hook-form@7.60.0: + resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@15.5.2: resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==} peerDependencies: @@ -2782,6 +2808,9 @@ packages: three@0.177.0: resolution: {integrity: sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==} + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2790,6 +2819,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -2848,6 +2880,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3057,6 +3093,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yup@1.6.1: + resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==} + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -3365,6 +3404,11 @@ snapshots: '@shikijs/types': 3.7.0 '@shikijs/vscode-textmate': 10.0.2 + '@hookform/resolvers@5.1.1(react-hook-form@7.60.0(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.60.0(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -3797,6 +3841,8 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/utils@0.3.0': {} + '@stoplight/better-ajv-errors@1.0.3(ajv@8.17.1)': dependencies: ajv: 8.17.1 @@ -5506,6 +5552,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-expr@2.0.6: {} + proxy-from-env@1.1.0: {} punycode.js@2.3.1: {} @@ -5566,6 +5614,10 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + react-hook-form@7.60.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-i18next@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): dependencies: '@babel/runtime': 7.27.3 @@ -5932,6 +5984,8 @@ snapshots: three@0.177.0: {} + tiny-case@1.0.3: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.4(picomatch@4.0.2) @@ -5941,6 +5995,8 @@ snapshots: dependencies: is-number: 7.0.0 + toposort@2.0.2: {} + tr46@0.0.3: {} troika-three-text@0.52.4(three@0.177.0): @@ -5987,6 +6043,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@2.19.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -6196,6 +6254,13 @@ snapshots: yocto-queue@0.1.0: {} + yup@1.6.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + zustand@4.5.7(@types/react@19.1.6)(immer@9.0.21)(react@19.1.0): dependencies: use-sync-external-store: 1.5.0(react@19.1.0) diff --git a/src/App.tsx b/src/App.tsx index db55bd4..6fdc37d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,15 @@ 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'; +import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func'; +import { refreshToken } from './pages/auth/service/Service'; + +declare global { + interface Window { + isRefreshingToken?: boolean; + } +} + i18next.init({ interpolation: { escapeValue: false }, lng: 'fa', @@ -26,9 +35,54 @@ i18next.init({ const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: async (error: IApiErrorRepsonse) => { - if (error?.response?.statusCode === 401) { - console.log(401); + if (error?.response?.status === 401) { + try { + // Use a flag to track if refresh is in progress + if (window.isRefreshingToken) { + // Wait for the refresh to complete + await new Promise(resolve => { + const checkComplete = setInterval(() => { + if (!window.isRefreshingToken) { + clearInterval(checkComplete); + resolve(true); + } + }, 100); + }); + return; + } + // Set the flag to indicate refresh is in progress + window.isRefreshingToken = true; + + try { + const refreshTokenValue = await getRefreshToken(); + const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' }); + + if (data?.accessToken?.token) { + // Save the new token + await setToken(data?.accessToken?.token); + + // If refresh token also returned, update it + if (data.refreshToken?.token) { + await setRefreshToken(data.refreshToken?.token); + } + + // Retry the failed request + queryClient.invalidateQueries(); + return; + } else { + throw new Error('Invalid token response'); + } + } finally { + window.isRefreshingToken = false; + } + } catch (refreshError: unknown) { + console.error('Token refresh failed:', refreshError); + // Clear tokens and redirect to login + await removeToken(); + await removeRefreshToken(); + window.location.href = '/auth/login'; + } } } }), diff --git a/src/config/func.ts b/src/config/func.ts index 1dc7af7..093bee3 100644 --- a/src/config/func.ts +++ b/src/config/func.ts @@ -59,3 +59,8 @@ export const timeAgo = (date: string | Date): string => { return "همین الان"; }; + +export const convertToGB = (bytes: number): string => { + const GB = bytes / (1024 * 1024 * 1024); + return GB.toFixed(2); +}; diff --git a/src/langs/fa.json b/src/langs/fa.json index f7b5adb..69146f6 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -17,6 +17,9 @@ "header": { "search": "جستجو" }, + "errors": { + "required": "این فیلد اجباری است" + }, "received": { "title": "دریافتی ها", "from_date": "از تاریخ", @@ -116,7 +119,9 @@ "signature_description": "تصویر و توضیحات امضای خود را با دقت وارد کنید", "upload_sign": "آپلود امضا", "description_sign": "توضیحات امضا", - "select_section": "لطفا یک بخش انتخاب کنید" + "select_section": "لطفا یک بخش انتخاب کنید", + "title1": "عنوان", + "quota": "فضا/ استفاده شده " }, "domain": { "table": { diff --git a/src/pages/auth/service/Service.ts b/src/pages/auth/service/Service.ts new file mode 100644 index 0000000..5c8c7cb --- /dev/null +++ b/src/pages/auth/service/Service.ts @@ -0,0 +1,7 @@ +import axiosDanak from "@/config/axiosDanak"; +import { RefreshTokenType } from "../types/Types"; + +export const refreshToken = async (params: RefreshTokenType) => { + const { data } = await axiosDanak.post(`/auth/refresh`, params); + return data; +}; diff --git a/src/pages/auth/types/Types.ts b/src/pages/auth/types/Types.ts new file mode 100644 index 0000000..5620433 --- /dev/null +++ b/src/pages/auth/types/Types.ts @@ -0,0 +1,3 @@ +export type RefreshTokenType = { + refreshToken: string; +}; diff --git a/src/pages/setting/address/Address.tsx b/src/pages/setting/address/Address.tsx index 5570a95..912537a 100644 --- a/src/pages/setting/address/Address.tsx +++ b/src/pages/setting/address/Address.tsx @@ -1,54 +1,47 @@ import Input from '@/components/Input' import Select from '@/components/Select' import Table from '@/components/Table' -import { Switch } from "@/components/ui/switch" import { ColumnType } from '@/components/types/TableTypes' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/components/Button' import { TickCircle } from 'iconsax-react' +import { useForm } from 'react-hook-form' +import { AddressData, CreateAddressType } from './types/Types' +import { useGetDomains } from '../domain/hooks/useDomainData' +import { yupResolver } from "@hookform/resolvers/yup" +import * as yup from "yup" +import { useCreateAddress, useGetAddress } from './hooks/useAddressData' +import { toast } from '@/components/Toast' +import { ErrorType } from '@/helpers/types' +import ProgressBarEmail from './components/ProgressBarEmail' +import ToggleStatus from './components/ToggleStatus' + -interface AddressData extends Record { - id: number - title: string - email: string - status: boolean -} const Address: FC = () => { const { t } = useTranslation() - const [addresses] = useState([ - { - id: 1, - title: 'بازاریابی', - email: 'Marketing@example.com', - status: true - }, - { - id: 2, - title: 'بازاریابی', - email: 'Marketing@example.com', - status: true - }, - { - id: 3, - title: 'بازاریابی', - email: 'Marketing@example.com', - status: true - }, - { - id: 4, - title: 'بازاریابی', - email: 'Marketing@example.com', - status: true - }, - { - id: 5, - title: 'بازاریابی', - email: 'Marketing@example.com', - status: true + + const schema = yup.object().shape({ + title: yup.string().required(t('errors.required')), + username: yup.string().required(t('errors.required')), + password: yup.string().required(t('errors.required')), + domainId: yup.string().required(t('errors.required')), + }) + + const [search, setSearch] = useState('') + const [status, setStatus] = useState('all') + + const { data: domain } = useGetDomains() + const { mutate: createAddress, isPending } = useCreateAddress() + const { data: addresses, isPending: isPendingAddress } = useGetAddress(search, status) + const { register, handleSubmit, formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + domainId: domain?.data?.domain?.id || '', } - ]) + }) const columns: ColumnType[] = [ { @@ -59,7 +52,23 @@ const Address: FC = () => { { title: t('setting.email'), key: 'email', - width: '300px' + width: '300px', + render: (item: AddressData) => ( +
+ {item.emailAddress} +
+ ) + }, + { + title: t('setting.quota'), + key: 'quota', + width: '300px', + render: (item: AddressData) => { + + return ( + + ) + } }, { title: t('setting.status'), @@ -68,7 +77,7 @@ const Address: FC = () => { align: 'center', render: (item: AddressData) => (
- +
) } @@ -80,6 +89,17 @@ const Address: FC = () => { { value: 'inactive', label: t('setting.inactive') } ] + const onSubmit = (params: CreateAddressType) => { + createAddress(params, { + onSuccess: (data) => { + toast(data?.data?.message, 'success') + }, + onError: (error: ErrorType) => { + toast(error.response?.data?.error?.message[0], 'error') + } + }) + } + return (
@@ -89,6 +109,7 @@ const Address: FC = () => { placeholder={t('setting.status')} items={statusOptions} className='w-[120px]' + onChange={(e) => setStatus(e.target.value)} />
@@ -97,13 +118,15 @@ const Address: FC = () => { variant='search' placeholder={t('setting.search')} className='xl:w-[300px] flex-1' + onChangeSearchFinal={(e) => setSearch(e)} />
{ {t('setting.add_address')} -
+ {/*
{t('setting.status')}
-
+
*/}
@@ -144,9 +169,11 @@ const Address: FC = () => {
- @ example.com + @ {domain?.data?.domain?.name}
@@ -154,6 +181,8 @@ const Address: FC = () => { @@ -170,6 +199,8 @@ const Address: FC = () => {
-
+
-
+