login
This commit is contained in:
Generated
+101
@@ -8,6 +8,7 @@
|
|||||||
"name": "shop",
|
"name": "shop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
@@ -22,8 +23,12 @@
|
|||||||
"next": "15.4.5",
|
"next": "15.4.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
|
"react-otp-input": "^3.1.1",
|
||||||
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"yup": "^1.7.0",
|
||||||
"zustand": "^5.0.7"
|
"zustand": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -240,6 +245,18 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/utils": "^0.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.55.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -1402,6 +1419,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -5632,6 +5655,12 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/property-expr": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -5690,12 +5719,48 @@
|
|||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.62.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||||
|
"integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-otp-input": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.6 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-spinners": {
|
||||||
|
"version": "0.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.17.0.tgz",
|
||||||
|
"integrity": "sha512-L/8HTylaBmIWwQzIjMq+0vyaRXuoAevzWoD35wKpNTxxtYXWZp+xtgkfD7Y4WItuX0YvdxMPU79+7VhhmbmuTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -6370,6 +6435,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-case": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
@@ -6428,6 +6499,12 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toposort": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
@@ -6483,6 +6560,18 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||||
|
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typed-array-buffer": {
|
"node_modules/typed-array-buffer": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||||
@@ -6784,6 +6873,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yup": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-VJce62dBd+JQvoc+fCVq+KZfPHr+hXaxCcVgotfwWvlR0Ja3ffYKaJBT8rptPOSKOGJDCUnW2C2JWpud7aRP6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"property-expr": "^2.0.5",
|
||||||
|
"tiny-case": "^1.0.3",
|
||||||
|
"toposort": "^2.0.2",
|
||||||
|
"type-fest": "^2.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
@@ -23,8 +24,12 @@
|
|||||||
"next": "15.4.5",
|
"next": "15.4.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
|
"react-hook-form": "^7.62.0",
|
||||||
|
"react-otp-input": "^3.1.1",
|
||||||
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"yup": "^1.7.0",
|
||||||
"zustand": "^5.0.7"
|
"zustand": "^5.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="80" height="131" viewBox="0 0 80 131" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.9251 0.338654C7.88357 1.20929 3.84207 4.02603 1.89297 7.32931C-0.113446 10.7606 0.00120607 6.63792 0.00120607 65.4824C0.00120607 124.301 -0.113446 120.204 1.89297 123.61C3.98538 127.195 8.45683 130.114 12.8136 130.754C14.1895 130.959 22.3298 131.036 41.1328 130.985L67.5029 130.908L69.366 130.319C74.4967 128.706 78.5955 124.609 79.656 120.076C80.1147 118.156 80.1147 12.8092 79.656 10.8886C78.5955 6.35624 74.6687 2.38719 69.5666 0.722748L67.5029 0.0569763L40.5595 0.00575256C18.0303 -0.0198517 13.3296 0.0313721 11.9251 0.338654ZM66.6143 6.20261C68.6207 6.66353 70.5412 7.81583 71.6877 9.22421C73.4935 11.452 73.3788 7.71341 73.3788 65.4824C73.3788 123.251 73.4935 119.513 71.6877 121.741C71.2004 122.33 70.2545 123.149 69.538 123.584C66.9296 125.197 67.4742 125.172 39.241 125.095L13.6162 125.018L12.0111 124.352C9.97598 123.507 8.42817 122.125 7.48228 120.307L6.73704 118.873V65.4824V12.0922L7.48228 10.6582C8.7148 8.32797 10.9505 6.66353 13.6735 6.10018C14.2754 5.97215 26.056 5.86972 39.843 5.84412C61.369 5.81851 65.1812 5.86972 66.6143 6.20261Z" fill="#CCCCCC"/>
|
||||||
|
<path d="M30.2613 17.5146C28.8686 18.6669 28.7709 20.7155 30.0903 22.0214L30.8233 22.764L40.1082 22.8408C50.3461 22.9176 50.6881 22.8664 51.4456 21.33C52.0809 20.0753 51.9098 18.8462 50.9814 17.8731L50.1506 17.0025H40.4992C31.2876 17.0025 30.8477 17.0281 30.2613 17.5146Z" fill="#CCCCCC"/>
|
||||||
|
<path d="M38.2529 102.695C35.5407 104.053 34.2701 106.844 35.052 109.66C35.8095 112.349 37.8131 113.962 40.4031 113.962C43.4329 113.962 45.6808 111.914 46.0962 108.764C46.6826 104.411 42.0157 100.8 38.2529 102.695Z" fill="#CCCCCC"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,73 @@
|
|||||||
|
import Input from '@/components/Input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import React from 'react'
|
||||||
|
import { useAuthStore } from '../store/AuthStore';
|
||||||
|
import { useAuthentication } from '../hooks/useAuthData';
|
||||||
|
import { AuthenticationType } from '../types/Types';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import * as yup from "yup"
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup"
|
||||||
|
import { toast } from '@/components/Toast';
|
||||||
|
import { extractErrorMessage } from '@/helpers/errorUtils';
|
||||||
|
import LoginStep2 from './LoginStep2';
|
||||||
|
|
||||||
|
|
||||||
|
const LoginStep1 = () => {
|
||||||
|
|
||||||
|
const { step, setStep, setPhone } = useAuthStore();
|
||||||
|
const { mutate: authentication, isPending } = useAuthentication();
|
||||||
|
|
||||||
|
const validationSchema = yup
|
||||||
|
.object({
|
||||||
|
phone: yup.string().required('شماره تلفن خود را وارد کنید'),
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<AuthenticationType>({
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: AuthenticationType) => {
|
||||||
|
authentication(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast("کد با موفقیت ارسال شد", "success")
|
||||||
|
setPhone(watch("phone"))
|
||||||
|
setStep(2)
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 2) {
|
||||||
|
return (
|
||||||
|
<LoginStep2 />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex-1 flex flex-col justify-center'>
|
||||||
|
<div className='text-[#CCCCCC] text-2xl font-black'>ورود و ثبت نام</div>
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Input
|
||||||
|
placeholder='09120000000'
|
||||||
|
className='text-left h-11'
|
||||||
|
{...register("phone")}
|
||||||
|
error_text={errors.phone?.message}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Button isLoading={isPending} onClick={handleSubmit(onSubmit)} className='w-full h-11'>دریافت کد</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginStep1
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { FC, useEffect } from 'react'
|
||||||
|
import { useAuthentication, useVerifyOtp } from '../hooks/useAuthData'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import OTPInput from 'react-otp-input'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { VerifyOtpType } from '../types/Types'
|
||||||
|
import * as yup from "yup"
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup"
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import { toast } from '@/components/Toast'
|
||||||
|
import { extractErrorMessage } from '@/helpers/errorUtils'
|
||||||
|
import Error from '@/components/Error'
|
||||||
|
import { setRefreshToken, setToken } from '@/config/func'
|
||||||
|
import { useCountDown } from '@/hooks/useCountDown'
|
||||||
|
|
||||||
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
|
const { phone } = useAuthStore()
|
||||||
|
const { isPending, mutate: verifyOtp } = useVerifyOtp()
|
||||||
|
|
||||||
|
const { mutate: authentication, isPending: isAuthenticationPending } = useAuthentication();
|
||||||
|
|
||||||
|
const { value: countdownValue, reset: resetCountdown } = useCountDown(2, true, () => {
|
||||||
|
// When countdown ends, enable resend button
|
||||||
|
})
|
||||||
|
|
||||||
|
const validationSchema = yup
|
||||||
|
.object({
|
||||||
|
otpCode: yup.string().required('کد را وارد کنید'),
|
||||||
|
phone: yup.string().required('شماره تلفن خود را وارد کنید'),
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<VerifyOtpType>({
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
phone: phone,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = (data: VerifyOtpType) => {
|
||||||
|
verifyOtp(data, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
toast('ورود با موفقیت انجام شد', 'success')
|
||||||
|
setToken(data?.results?.data?.accessToken?.token)
|
||||||
|
setRefreshToken(data?.results?.data?.refreshToken?.token)
|
||||||
|
window.location.href = '/'
|
||||||
|
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast(extractErrorMessage(error), 'error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResendOtp = () => {
|
||||||
|
authentication({
|
||||||
|
phone: phone,
|
||||||
|
}, {
|
||||||
|
onSuccess: () => {
|
||||||
|
resetCountdown()
|
||||||
|
toast('کد جدید ارسال شد', 'success')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (watch('otpCode')?.length === 5) {
|
||||||
|
onSubmit({
|
||||||
|
otpCode: watch('otpCode'),
|
||||||
|
phone: phone,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [watch('otpCode')])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex-1 flex flex-col justify-center'>
|
||||||
|
<div className='text-[#CCCCCC] text-2xl font-black'>کد دریافتی را وارد نمایید</div>
|
||||||
|
<div className='mt-8 dltr otp'>
|
||||||
|
<OTPInput
|
||||||
|
shouldAutoFocus
|
||||||
|
value={watch("otpCode")}
|
||||||
|
numInputs={5}
|
||||||
|
inputType='tel'
|
||||||
|
containerStyle={{ direction: 'ltr' }}
|
||||||
|
onChange={(otp: string) => setValue('otpCode', otp)}
|
||||||
|
renderInput={(props) =>
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
type='tel'
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-[10px]' />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
errors.otpCode?.message && <Error errorText={errors.otpCode.message} />
|
||||||
|
}
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Button isLoading={isPending} className='w-full h-11' onClick={handleSubmit(onSubmit)}>تایید</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countdown and Resend Section */}
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<span className='text-[#CCCCCC] text-sm'>کد فعالسازی را دریافت نکردید؟</span>
|
||||||
|
{countdownValue !== '00:00' ? (
|
||||||
|
<span className='text-blue-500 font-medium text-sm'>{countdownValue}</span>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant={'ghost'}
|
||||||
|
isLoading={isAuthenticationPending}
|
||||||
|
onClick={handleResendOtp}
|
||||||
|
className='text-blue-500 hover:text-blue-600 cursor-pointer text-sm font-medium transition-colors'
|
||||||
|
>
|
||||||
|
دوباره امتحان کنید
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginStep2
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { authentication, verifyOtp } from "../service/Service";
|
||||||
|
|
||||||
|
export const useAuthentication = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: authentication,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useVerifyOtp = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: verifyOtp,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
'use client'
|
||||||
|
import { NextPage } from 'next'
|
||||||
|
import Image from 'next/image';
|
||||||
|
import LoginStep1 from './components/LoginStep1';
|
||||||
|
|
||||||
|
const LoginPage: NextPage = () => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex justify-center items-center h-screen'>
|
||||||
|
<div className='h-[450px] w-[1000px] border border-border rounded-xl flex p-10 gap-32'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='text-primary'>
|
||||||
|
1/2
|
||||||
|
</div>
|
||||||
|
<div className='w-full h-0.5 mt-4 flex items-center'>
|
||||||
|
<div className='size-2 rounded-full bg-primary'></div>
|
||||||
|
<div className='w-1/2 h-full bg-primary'></div>
|
||||||
|
<div className='size-2 rounded-full bg-primary'></div>
|
||||||
|
|
||||||
|
<div className='w-1/2 h-full bg-primary/40'></div>
|
||||||
|
<div className='size-2 rounded-full bg-primary/40'></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-20 flex gap-6'>
|
||||||
|
<Image
|
||||||
|
src='/images/mobile.svg'
|
||||||
|
width={80}
|
||||||
|
height={131}
|
||||||
|
alt='phone'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className='text-[#8B8B8B] text-xl'>Wellcome</div>
|
||||||
|
<div className='mt-2 font-black text-4xl text-[#4F4F4F]'>
|
||||||
|
خوش آمدید
|
||||||
|
</div>
|
||||||
|
<p className='mt-4 text-[#8B8B8B] text-sm'>
|
||||||
|
برای دریافت کد فعالسازی شماره همراه یا ایمیل خود را وارد نمایید
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LoginStep1 />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginPage
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import { AuthenticationType, VerifyOtpType } from "../types/Types";
|
||||||
|
|
||||||
|
export const authentication = async (params: AuthenticationType) => {
|
||||||
|
const { data } = await axios.post("/user/authenticate", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyOtp = async (params: VerifyOtpType) => {
|
||||||
|
const { data } = await axios.post("/user/login/otp", params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { AuthStoreType } from "../types/Types";
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||||
|
step: 1,
|
||||||
|
setStep: (step) => set({ step }),
|
||||||
|
phone: "",
|
||||||
|
setPhone: (phone) => set({ phone }),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type AuthStoreType = {
|
||||||
|
step: number;
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
phone: string;
|
||||||
|
setPhone: (phone: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthenticationType = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VerifyOtpType = {
|
||||||
|
phone: string;
|
||||||
|
otpCode: string;
|
||||||
|
};
|
||||||
@@ -187,3 +187,11 @@ textarea.place-black::placeholder {
|
|||||||
div {
|
div {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.otp div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dltr {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import "./globals.css";
|
|||||||
import "../../public/css/fontiran.css";
|
import "../../public/css/fontiran.css";
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import QueryProvider from "@/config/QueryProvider";
|
import QueryProvider from "@/config/QueryProvider";
|
||||||
|
import ToastContainer from "@/components/Toast";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
@@ -19,6 +20,7 @@ export default function RootLayout({
|
|||||||
<body>
|
<body>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
{children}
|
{children}
|
||||||
|
<ToastContainer />
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
import { CloseCircle, InfoCircle, TickCircle } from 'iconsax-react';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
interface Toast {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
type?: 'success' | 'error' | 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
let addToast: (toast: Toast) => void;
|
||||||
|
|
||||||
|
const ToastContainer: React.FC = () => {
|
||||||
|
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||||
|
|
||||||
|
// ثبت toast جدید
|
||||||
|
const showToast = (toast: Toast) => {
|
||||||
|
setToasts((prev) => [...prev, toast]);
|
||||||
|
|
||||||
|
// حذف خودکار بعد از ۳ ثانیه
|
||||||
|
setTimeout(() => {
|
||||||
|
setToasts((prev) => prev.filter((t) => t.id !== toast.id));
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// تخصیص تابع نمایش toast به متغیر سراسری
|
||||||
|
useEffect(() => {
|
||||||
|
addToast = showToast;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed top-4 text-sm right-0 left-0 mx-auto w-fit z-50 flex flex-col gap-2">
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={`px-4 flex items-center gap-2 backdrop-blur-2xl h-16 min-w-[300px] rounded-2xl shadow-md transition-transform ${toast.type === 'success'
|
||||||
|
? 'bg-white/70 text-black'
|
||||||
|
: toast.type === 'error'
|
||||||
|
? 'bg-white/70 text-black'
|
||||||
|
: 'bg-white/70 text-black'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
toast.type === 'success' ? <TickCircle className='size-5' color='green' /> :
|
||||||
|
toast.type === 'error' ? <CloseCircle className='size-5' color='red' /> :
|
||||||
|
<InfoCircle className='size-5' color='blue' />
|
||||||
|
}
|
||||||
|
{toast.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||||
|
addToast({ id: Date.now().toString(), message, type });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToastContainer;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { ClipLoader } from "react-spinners"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
@@ -40,10 +41,14 @@ function Button({
|
|||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
|
isLoading = false,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
@@ -51,8 +56,18 @@ function Button({
|
|||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
disabled={disabled || isLoading}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<ClipLoader size={16} color="currentColor" />
|
||||||
|
<span>در حال بارگذاری...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Comp>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { IApiErrorRepsonse } from '@/types/error.types'
|
import { IGeneralError } from '@/types/error.types'
|
||||||
import { QueryClient, QueryClientProvider, HydrationBoundary, QueryCache } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider, HydrationBoundary, QueryCache } from '@tanstack/react-query'
|
||||||
import { ReactNode, useState } from 'react'
|
import { ReactNode, useState } from 'react'
|
||||||
import { removeToken } from './func'
|
import { removeToken } from './func'
|
||||||
@@ -10,8 +10,9 @@ export default function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
const { setIsLogin } = useSharedStore()
|
const { setIsLogin } = useSharedStore()
|
||||||
const [client] = useState(() => new QueryClient({
|
const [client] = useState(() => new QueryClient({
|
||||||
queryCache: new QueryCache({
|
queryCache: new QueryCache({
|
||||||
onError: async (error: IApiErrorRepsonse) => {
|
onError: async (error: Error) => {
|
||||||
if (error.response?.status === 401) {
|
const axiosError = error as { response?: { status: number; data: IGeneralError } };
|
||||||
|
if (axiosError.response?.status === 401) {
|
||||||
removeToken()
|
removeToken()
|
||||||
setIsLogin(false)
|
setIsLogin(false)
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
export const TOKEN_NAME = "sh_token";
|
export const TOKEN_NAME = "sh_token";
|
||||||
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
||||||
export const BASE_URL = "https://api.shinan.ir";
|
// export const BASE_URL = "https://api.shinan.ir";
|
||||||
|
export const BASE_URL = "https://api-sondos.run.danakcorp.com";
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { IGeneralError } from "@/types/error.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* استخراج پیام خطا از error object
|
||||||
|
* @param error - error object از React Query یا Axios
|
||||||
|
* @param fallbackMessage - پیام پیشفرض در صورت عدم وجود خطا
|
||||||
|
* @returns پیام خطا
|
||||||
|
*/
|
||||||
|
export const extractErrorMessage = (
|
||||||
|
error: Error | unknown,
|
||||||
|
fallbackMessage: string = "خطایی رخ داده است"
|
||||||
|
): string => {
|
||||||
|
try {
|
||||||
|
const axiosError = error as { response?: { data: IGeneralError } };
|
||||||
|
return axiosError?.response?.data?.error?.details?.[0] || fallbackMessage;
|
||||||
|
} catch {
|
||||||
|
return fallbackMessage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* بررسی اینکه آیا error object معتبر است یا نه
|
||||||
|
* @param error - error object
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export const isValidError = (
|
||||||
|
error: unknown
|
||||||
|
): error is { response?: { data: IGeneralError } } => {
|
||||||
|
return !!(
|
||||||
|
typeof error === "object" &&
|
||||||
|
error !== null &&
|
||||||
|
"response" in error &&
|
||||||
|
error.response &&
|
||||||
|
typeof error.response === "object" &&
|
||||||
|
"data" in error.response &&
|
||||||
|
error.response.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"error" in error.response.data
|
||||||
|
);
|
||||||
|
};
|
||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface ICountDown {
|
||||||
|
m: number;
|
||||||
|
s: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param minutes how many minutes.
|
||||||
|
* @param autoplay by default true. if you want play coundDown after an action, you can set it to false and manually play it.
|
||||||
|
* @param onEnd you can pass a callback function that will trigger at the END.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useCountDown(minutes: number, autoplay = true, onEnd = () => {}) {
|
||||||
|
|
||||||
|
const [countDown, setCountDown] = useState<ICountDown>({m: minutes, s: 0});
|
||||||
|
const [stop, setStop] = useState<boolean>(!autoplay);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
if (countDown.m !== 0 || countDown.s !== 0) {
|
||||||
|
setCountDown(calculateCountDown(countDown));
|
||||||
|
} else {
|
||||||
|
onEnd();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler);
|
||||||
|
};
|
||||||
|
}, [countDown]);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setCountDown({m: minutes, s: 0});
|
||||||
|
};
|
||||||
|
const pause = () => {
|
||||||
|
setStop(true);
|
||||||
|
};
|
||||||
|
const play = () => {
|
||||||
|
setStop(false);
|
||||||
|
setCountDown({...countDown});
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateCountDown = (st: ICountDown): ICountDown => {
|
||||||
|
if (stop) {
|
||||||
|
return countDown;
|
||||||
|
}
|
||||||
|
const timeLeft = { m: 0, s: 0 };
|
||||||
|
const remain = st.m * 60 + st.s - 1;
|
||||||
|
if (remain > 0) {
|
||||||
|
timeLeft.m = Math.floor((remain) / 60);
|
||||||
|
timeLeft.s = remain - timeLeft.m * 60;
|
||||||
|
return timeLeft;
|
||||||
|
} else {
|
||||||
|
if (remain === 0 && countDown.s === 1) {
|
||||||
|
return timeLeft;
|
||||||
|
} else {
|
||||||
|
return countDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: `${countDown.m > 9 ? countDown.m : '0' + countDown.m}:${countDown.s > 9 ? countDown.s : '0' + countDown.s}`,
|
||||||
|
reset,
|
||||||
|
pause,
|
||||||
|
play
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# سیستم مدیریت خطا
|
||||||
|
|
||||||
|
## ساختار خطاها
|
||||||
|
|
||||||
|
خطاهای API در این پروژه دارای ساختار زیر هستند:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"status": 400,
|
||||||
|
"success": false,
|
||||||
|
"error": {
|
||||||
|
"message": "Bad request",
|
||||||
|
"details": ["فرمت موبایل اشتباه است"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type های تعریف شده
|
||||||
|
|
||||||
|
### IGeneralError
|
||||||
|
Type اصلی برای خطاهای API:
|
||||||
|
```typescript
|
||||||
|
interface IGeneralError {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
error: {
|
||||||
|
message: string;
|
||||||
|
details: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IQueryError
|
||||||
|
Type برای خطاهای React Query:
|
||||||
|
```typescript
|
||||||
|
interface IQueryError {
|
||||||
|
response?: {
|
||||||
|
data: IGeneralError;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## نحوه استفاده
|
||||||
|
|
||||||
|
### 1. استفاده مستقیم در onError
|
||||||
|
```typescript
|
||||||
|
onError: (error: Error) => {
|
||||||
|
const axiosError = error as { response?: { data: IGeneralError } };
|
||||||
|
const errorMessage = axiosError?.response?.data?.error?.details?.[0] || 'خطایی رخ داده است';
|
||||||
|
toast(errorMessage, 'error');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. استفاده از utility function
|
||||||
|
```typescript
|
||||||
|
import { extractErrorMessage } from '@/helpers/errorUtils';
|
||||||
|
|
||||||
|
onError: (error: Error) => {
|
||||||
|
const errorMessage = extractErrorMessage(error);
|
||||||
|
toast(errorMessage, 'error');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. استفاده با fallback message
|
||||||
|
```typescript
|
||||||
|
const errorMessage = extractErrorMessage(error, 'خطای پیشفرض');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utility Functions
|
||||||
|
|
||||||
|
### extractErrorMessage
|
||||||
|
این تابع پیام خطا را از error object استخراج میکند:
|
||||||
|
```typescript
|
||||||
|
export const extractErrorMessage = (
|
||||||
|
error: Error | unknown,
|
||||||
|
fallbackMessage: string = 'خطایی رخ داده است'
|
||||||
|
): string
|
||||||
|
```
|
||||||
|
|
||||||
|
### isValidError
|
||||||
|
این تابع بررسی میکند که آیا error object معتبر است یا نه:
|
||||||
|
```typescript
|
||||||
|
export const isValidError = (error: unknown): error is { response?: { data: IGeneralError } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
1. همیشه از `error?.response?.data?.error?.details?.[0]` برای دسترسی به پیام خطا استفاده کنید
|
||||||
|
2. از `fallback message` برای نمایش پیام پیشفرض در صورت عدم وجود خطا استفاده کنید
|
||||||
|
3. از utility functions برای مدیریت بهتر خطاها استفاده کنید
|
||||||
|
4. Type safety را با استفاده از type assertion حفظ کنید
|
||||||
@@ -1,14 +1,36 @@
|
|||||||
import { DefaultError } from "@tanstack/react-query";
|
import { DefaultError } from "@tanstack/react-query";
|
||||||
import { IBaseResponse } from "./response.types";
|
import { IBaseResponse } from "./response.types";
|
||||||
|
|
||||||
interface IError {
|
// ساختار جدید خطا بر اساس نمونه کاربر
|
||||||
message: string[];
|
interface IErrorDetails {
|
||||||
|
message: string;
|
||||||
|
details: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IErrorResponse extends IBaseResponse {
|
export interface IErrorResponse extends IBaseResponse {
|
||||||
error: IError;
|
error: IErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApiErrorRepsonse extends DefaultError {
|
// Type برای خطاهای API
|
||||||
response?: IErrorResponse;
|
export interface IApiErrorResponse extends DefaultError {
|
||||||
|
response?: {
|
||||||
|
data: IErrorResponse;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type عمومی برای خطاها
|
||||||
|
export interface IGeneralError {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
error: {
|
||||||
|
message: string;
|
||||||
|
details: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type برای خطاهای React Query
|
||||||
|
export interface IQueryError {
|
||||||
|
response?: {
|
||||||
|
data: IGeneralError;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user