category service
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
VITE_TOKEN_NAME = 'admin_token'
|
VITE_TOKEN_NAME = 'admin_token'
|
||||||
|
VITE_REFRESH_TOKEN_NAME = 'admin_refresh_token'
|
||||||
|
VITE_BASE_URL = 'http://192.168.0.223:3500'
|
||||||
Generated
+111
-1
@@ -11,6 +11,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@material-tailwind/react": "^2.1.10",
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@tanstack/react-query": "^5.62.8",
|
"@tanstack/react-query": "^5.62.8",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"draft-js": "^0.11.7",
|
"draft-js": "^0.11.7",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-router-dom": "^7.1.0",
|
"react-router-dom": "^7.1.0",
|
||||||
|
"react-spinners": "^0.15.0",
|
||||||
"react-toastify": "^11.0.2",
|
"react-toastify": "^11.0.2",
|
||||||
"swiper": "^11.2.1",
|
"swiper": "^11.2.1",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
@@ -2259,6 +2261,12 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/attr-accept": {
|
"node_modules/attr-accept": {
|
||||||
"version": "2.2.5",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||||
@@ -2306,6 +2314,17 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||||
|
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -2514,6 +2533,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@@ -2635,6 +2666,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -3096,6 +3136,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||||
@@ -3113,6 +3173,20 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/formik": {
|
"node_modules/formik": {
|
||||||
"version": "2.4.6",
|
"version": "2.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
|
||||||
@@ -3401,7 +3475,6 @@
|
|||||||
"version": "3.7.6",
|
"version": "3.7.6",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz",
|
||||||
"integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==",
|
"integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=0.8.0"
|
||||||
@@ -3748,6 +3821,27 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -4247,6 +4341,12 @@
|
|||||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -4517,6 +4617,16 @@
|
|||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-spinners": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA==",
|
||||||
|
"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/react-toastify": {
|
"node_modules/react-toastify": {
|
||||||
"version": "11.0.2",
|
"version": "11.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.2.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@material-tailwind/react": "^2.1.10",
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@tanstack/react-query": "^5.62.8",
|
"@tanstack/react-query": "^5.62.8",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"draft-js": "^0.11.7",
|
"draft-js": "^0.11.7",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"react-multi-date-picker": "^4.5.2",
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-router-dom": "^7.1.0",
|
"react-router-dom": "^7.1.0",
|
||||||
|
"react-spinners": "^0.15.0",
|
||||||
"react-toastify": "^11.0.2",
|
"react-toastify": "^11.0.2",
|
||||||
"swiper": "^11.2.1",
|
"swiper": "^11.2.1",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { IApiErrorRepsonse } from './types/error.types'
|
|||||||
import MainRouter from './router/Main'
|
import MainRouter from './router/Main'
|
||||||
import AuthRouter from './router/Auth'
|
import AuthRouter from './router/Auth'
|
||||||
import { Pages } from './config/Pages'
|
import { Pages } from './config/Pages'
|
||||||
|
|
||||||
i18next.init({
|
i18next.init({
|
||||||
interpolation: { escapeValue: false },
|
interpolation: { escapeValue: false },
|
||||||
lng: 'fa',
|
lng: 'fa',
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 201.8 46.7" xml:space="preserve" class="logo-shape dark" style="enable-background:new 0 0 201.8 46.7;"><g><g><path d="M51,38.6h-1.9v5.1h-0.8v-5.1h-1.9V38H51L51,38.6L51,38.6z" class="st0"></path> <path d="M57.6,43.8c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.3-0.6
|
||||||
|
c0.6,0,1,0.2,1.3,0.5s0.4,0.8,0.4,1.4v0.4h-2.7l0,0c0,0.4,0.1,0.8,0.3,1c0.2,0.3,0.5,0.4,0.9,0.4c0.3,0,0.5,0,0.7-0.1
|
||||||
|
c0.2-0.1,0.4-0.2,0.5-0.3l0.3,0.5c-0.1,0.1-0.4,0.3-0.6,0.4C58.3,43.7,58,43.8,57.6,43.8z M57.5,40c-0.3,0-0.5,0.1-0.7,0.3
|
||||||
|
c-0.2,0.2-0.3,0.5-0.3,0.8l0,0h1.9V41c0-0.3-0.1-0.6-0.2-0.8S57.8,40,57.5,40z" class="st0"></path> <path d="M66.3,43.2c0.2,0,0.5-0.1,0.6-0.2s0.3-0.4,0.3-0.6h0.7l0,0c0,0.4-0.1,0.7-0.5,1c-0.3,0.3-0.7,0.4-1.1,0.4
|
||||||
|
c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.4-0.6c0.5,0,0.9,0.1,1.2,0.4
|
||||||
|
c0.3,0.3,0.5,0.7,0.4,1.1l0,0h-0.7c0-0.3-0.1-0.5-0.3-0.7S66.5,40,66.3,40c-0.4,0-0.7,0.1-0.8,0.4c-0.2,0.3-0.3,0.6-0.3,1.1v0.2
|
||||||
|
c0,0.4,0.1,0.8,0.3,1.1C65.6,43,65.9,43.2,66.3,43.2z" class="st0"></path> <path d="M74,40c0.1-0.2,0.3-0.4,0.5-0.5s0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3v2.7H76V41
|
||||||
|
c0-0.4-0.1-0.6-0.2-0.8C75.6,40.1,75.3,40,75,40c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-6.1H74L74,40L74,40z" class="st0"></path> <path d="M83,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
|
||||||
|
v2.7H85V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L83,39.4
|
||||||
|
L83,39.4z" class="st0"></path> <path d="M91.2,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
|
||||||
|
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6c-0.6,0-1-0.2-1.4-0.6C91.4,42.8,91.2,42.3,91.2,41.5L91.2,41.5z M92,41.6
|
||||||
|
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1
|
||||||
|
c-0.2-0.3-0.5-0.5-0.9-0.5s-0.6,0.1-0.8,0.5S92,41.1,92,41.6L92,41.6z" class="st0"></path> <path d="M101.3,43.7h-0.8v-6.1h0.8V43.7z" class="st0"></path> <path d="M106.8,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
|
||||||
|
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C107,42.8,106.8,42.3,106.8,41.5L106.8,41.5z M107.6,41.6
|
||||||
|
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
|
||||||
|
s-0.6,0.1-0.8,0.5S107.6,41.1,107.6,41.6L107.6,41.6z" class="st0"></path> <path d="M115.9,41.7c0-0.7,0.1-1.2,0.4-1.7s0.7-0.6,1.2-0.6c0.3,0,0.5,0,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5l0.1-0.6h0.6
|
||||||
|
v4.3c0,0.5-0.2,1-0.5,1.3s-0.8,0.4-1.4,0.4c-0.2,0-0.4,0-0.7-0.1c-0.2-0.1-0.5-0.1-0.6-0.2l0.1-0.6c0.1,0.1,0.3,0.1,0.5,0.2
|
||||||
|
c0.2,0,0.4,0.1,0.6,0.1c0.4,0,0.6-0.1,0.8-0.3c0.2-0.2,0.3-0.4,0.3-0.8v-0.5c-0.1,0.2-0.3,0.3-0.5,0.4c-0.2,0.1-0.4,0.1-0.7,0.1
|
||||||
|
c-0.5,0-0.9-0.2-1.2-0.6S115.9,42.4,115.9,41.7L115.9,41.7L115.9,41.7z M116.7,41.7c0,0.4,0.1,0.8,0.3,1s0.5,0.4,0.8,0.4
|
||||||
|
c0.2,0,0.4,0,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.5v-2c-0.1-0.2-0.2-0.3-0.4-0.4c-0.2-0.1-0.4-0.2-0.6-0.2c-0.4,0-0.6,0.1-0.8,0.5
|
||||||
|
S116.7,41.2,116.7,41.7L116.7,41.7L116.7,41.7z" class="st0"></path> <path d="M126.4,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8c-0.2,0.2-0.5,0.3-0.8,0.3
|
||||||
|
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0c0.1,0,0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.3,0.3-0.5
|
||||||
|
l0.2-0.5l-1.6-4.2h0.9L126.4,42.1z" class="st0"></path> <path d="M144.4,41.9L144.4,41.9c0,0.6-0.2,1-0.6,1.4s-0.9,0.5-1.5,0.5c-0.6,0-1.2-0.2-1.6-0.7s-0.6-1-0.6-1.8v-1
|
||||||
|
c0-0.7,0.2-1.3,0.6-1.8s0.9-0.7,1.6-0.7s1.2,0.2,1.5,0.5c0.4,0.3,0.6,0.8,0.6,1.4l0,0h-0.8c0-0.4-0.1-0.7-0.4-1
|
||||||
|
c-0.2-0.2-0.6-0.4-1-0.4s-0.8,0.2-1,0.5c-0.3,0.4-0.4,0.8-0.4,1.3v1c0,0.5,0.1,1,0.4,1.3s0.6,0.5,1,0.5c0.4,0,0.8-0.1,1-0.4
|
||||||
|
s0.4-0.5,0.4-1L144.4,41.9L144.4,41.9z" class="st0"></path> <path d="M149.6,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
|
||||||
|
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C149.8,42.8,149.6,42.3,149.6,41.5L149.6,41.5z M150.4,41.6
|
||||||
|
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
|
||||||
|
s-0.6,0.1-0.8,0.5S150.4,41.1,150.4,41.6L150.4,41.6z" class="st0"></path> <path d="M159.5,39.4l0,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.8-0.2s0.5,0.1,0.7,0.2s0.4,0.3,0.5,0.6
|
||||||
|
c0.1-0.3,0.3-0.4,0.5-0.6c0.2-0.1,0.5-0.2,0.8-0.2c0.4,0,0.8,0.1,1,0.5c0.3,0.3,0.4,0.8,0.4,1.3v2.5H164v-2.6
|
||||||
|
c0-0.4-0.1-0.7-0.2-0.9c-0.1-0.2-0.4-0.3-0.6-0.3c-0.3,0-0.5,0.1-0.6,0.3c-0.2,0.2-0.3,0.4-0.3,0.7v0v2.7h-0.8v-2.6
|
||||||
|
c0-0.4-0.1-0.7-0.2-0.9s-0.4-0.3-0.6-0.3c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L159.5,39.4L159.5,39.4z" class="st0"></path> <path d="M174,41.7c0,0.6-0.1,1.1-0.4,1.5s-0.7,0.6-1.2,0.6c-0.3,0-0.5,0-0.7-0.1s-0.4-0.2-0.5-0.4v2.1h-0.8v-5.9h0.6
|
||||||
|
l0.1,0.5c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.9,0.2,1.2,0.6S174,41,174,41.7L174,41.7L174,41.7z M173.2,41.7
|
||||||
|
c0-0.5-0.1-0.9-0.3-1.2s-0.5-0.5-0.8-0.5c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v2.1c0.1,0.2,0.2,0.3,0.4,0.4s0.4,0.1,0.6,0.1
|
||||||
|
c0.4,0,0.6-0.1,0.8-0.4C173.1,42.5,173.2,42.2,173.2,41.7L173.2,41.7L173.2,41.7z" class="st0"></path> <path d="M182.1,43.7c0-0.1,0-0.2-0.1-0.3s0-0.2,0-0.3c-0.1,0.2-0.3,0.4-0.6,0.5s-0.5,0.2-0.8,0.2
|
||||||
|
c-0.4,0-0.8-0.1-1-0.3s-0.4-0.5-0.4-0.9c0-0.4,0.2-0.7,0.5-1c0.3-0.2,0.8-0.3,1.3-0.3h0.8v-0.4c0-0.3-0.1-0.5-0.2-0.6
|
||||||
|
s-0.4-0.2-0.6-0.2c-0.3,0-0.5,0.1-0.6,0.2c-0.2,0.1-0.2,0.3-0.2,0.5l-0.7,0l0,0c0-0.3,0.1-0.6,0.4-0.9s0.7-0.4,1.2-0.4
|
||||||
|
c0.5,0,0.9,0.1,1.2,0.4c0.3,0.3,0.4,0.6,0.4,1.1v2c0,0.1,0,0.3,0,0.4c0,0.1,0,0.3,0.1,0.4L182.1,43.7L182.1,43.7z M180.8,43.1
|
||||||
|
c0.3,0,0.5-0.1,0.8-0.2c0.2-0.1,0.4-0.3,0.4-0.5v-0.7h-0.9c-0.3,0-0.6,0.1-0.8,0.2s-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.5
|
||||||
|
S180.5,43.1,180.8,43.1z" class="st0"></path> <path d="M189.1,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
|
||||||
|
v2.7H191V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3
|
||||||
|
L189.1,39.4L189.1,39.4z" class="st0"></path> <path d="M198.7,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8s-0.5,0.3-0.8,0.3c-0.1,0-0.1,0-0.2,0
|
||||||
|
c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0s0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2s0.2-0.3,0.3-0.5l0.2-0.5l-1.6-4.2h0.9L198.7,42.1z" class="st0"></path></g></g> <g><path d="M46.4,31.9V9.9h6.2c1.9,0,3.6,0.4,5.1,1.3c1.5,0.9,2.6,2.1,3.4,3.6c0.8,1.6,1.2,3.4,1.2,5.4v1.4
|
||||||
|
c0,2.1-0.4,3.9-1.2,5.5c-0.8,1.6-2,2.8-3.4,3.6s-3.2,1.3-5.2,1.3H46.4z M49.3,12.2v17.3h3.1c2.2,0,4-0.7,5.2-2.1
|
||||||
|
c1.3-1.4,1.9-3.4,1.9-6v-1.3c0-2.5-0.6-4.5-1.8-5.9s-2.9-2.1-5-2.1L49.3,12.2L49.3,12.2z" class="st0"></path> <path d="M131.9,31.9H129l-11.1-17v17h-2.9V9.9h2.9L129,26.9V9.9h2.9L131.9,31.9L131.9,31.9z" class="st0"></path> <path d="M189.6,21.7l-2.7,2.8v7.5H184V9.8h2.9v10.9l9.8-10.9h3.5l-8.7,9.7l9.4,12.3h-3.5L189.6,21.7z" class="st0"></path> <polygon points="88.9,13.4 95.6,31.9 98.6,31.9 90.1,9.8 87.6,9.8 79.2,31.9 82.2,31.9 " class="st0"></polygon> <polygon points="157.9,13.4 164.7,31.9 167.6,31.9 159.2,9.8 156.7,9.8 148.2,31.9 151.2,31.9 " class="st0"></polygon></g> <path d="M27.5,30.2c0,0-5.5,5.3-10.1,9.8v6.1c-0.5,0.4,16.4-15.9,16.4-15.9L17.4,13v6.5L27.5,30.2z" class="st0"></path> <g><rect x="9.1" y="13.1" transform="matrix(0.7298 -0.6837 0.6837 0.7298 -14.5845 13.0725)" width="0.3" height="23.8" class="st0"></rect> <rect x="9.9" y="13.4" transform="matrix(0.7287 -0.6849 0.6849 0.7287 -13.8472 13.4064)" width="0.3" height="21.6" class="st0"></rect> <polygon points="17.3,30.5 4,16.4 4.2,16.2 17.5,30.3 " class="st0"></polygon> <polygon points="17.3,28.9 5.5,16.5 5.7,16.3 17.5,28.7 " class="st0"></polygon> <polygon points="17.2,27.2 7,16.5 7.2,16.3 17.5,27.1 " class="st0"></polygon></g> <g><polygon points="1.3,16.5 1.1,16.3 17.3,0.5 17.5,0.7 " class="st0"></polygon> <polygon points="2.8,16.5 2.6,16.3 17.3,2 17.4,2.2 " class="st0"></polygon> <polygon points="4.3,16.5 4.1,16.3 17.2,3.5 17.4,3.7 " class="st0"></polygon> <polygon points="5.8,16.5 5.6,16.3 17.2,5 17.4,5.2 " class="st0"></polygon> <polygon points="7.4,16.5 7.2,16.3 17.2,6.5 17.4,6.7 " class="st0"></polygon></g></svg>
|
||||||
|
After Width: | Height: | Size: 8.2 KiB |
@@ -1,5 +1,5 @@
|
|||||||
import { ButtonHTMLAttributes, FC, memo, ReactNode } from 'react'
|
import { ButtonHTMLAttributes, FC, memo, ReactNode } from 'react'
|
||||||
import ReactLoading from 'react-loading'
|
import MoonLoader from "react-spinners/MoonLoader"
|
||||||
import { XOR } from '../helpers/types'
|
import { XOR } from '../helpers/types'
|
||||||
import { clx } from '../helpers/utils'
|
import { clx } from '../helpers/utils'
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
} & ButtonHTMLAttributes<HTMLButtonElement> &
|
} & ButtonHTMLAttributes<HTMLButtonElement> &
|
||||||
XOR<{ children: ReactNode }, { label: string, isLoading?: boolean }>;
|
XOR<{ children: ReactNode }, { label: string }>;
|
||||||
|
|
||||||
const Button: FC<Props> = memo((props: Props) => {
|
const Button: FC<Props> = memo((props: Props) => {
|
||||||
|
|
||||||
@@ -18,10 +18,10 @@ const Button: FC<Props> = memo((props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button {...props} className={`${buttonClass} ${props.className}`} >
|
<button disabled={props.isLoading} {...props} className={`${buttonClass} ${props.className}`} >
|
||||||
{
|
{
|
||||||
props.isLoading ?
|
props.isLoading ?
|
||||||
<ReactLoading type='spin' color={'white'} width={20} height={20} />
|
<MoonLoader color="white" size={16} />
|
||||||
:
|
:
|
||||||
props.label || props.children
|
props.label || props.children
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { FC, InputHTMLAttributes, useState } from 'react'
|
import { FC, InputHTMLAttributes, useEffect, useState } from 'react'
|
||||||
import { clx } from '../helpers/utils';
|
import { clx } from '../helpers/utils';
|
||||||
import EyeIcon from '../assets/images/eye.svg'
|
import EyeIcon from '../assets/images/eye.svg'
|
||||||
import { SearchNormal } from 'iconsax-react';
|
import { SearchNormal } from 'iconsax-react';
|
||||||
|
import Error from './Error';
|
||||||
|
|
||||||
type Variant = "floating_outlined" | "primary" | "search";
|
type Variant = "floating_outlined" | "primary" | "search";
|
||||||
|
|
||||||
@@ -14,11 +15,13 @@ type Props = {
|
|||||||
unit?: string;
|
unit?: string;
|
||||||
seprator?: boolean;
|
seprator?: boolean;
|
||||||
isNotRequired?: boolean;
|
isNotRequired?: boolean;
|
||||||
|
onChangeSearchFinal?: (value: string) => void;
|
||||||
} & InputHTMLAttributes<HTMLInputElement>
|
} & InputHTMLAttributes<HTMLInputElement>
|
||||||
|
|
||||||
const Input: FC<Props> = (props: Props) => {
|
const Input: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
const [search, setSearch] = useState<string>('')
|
||||||
|
|
||||||
const inputClass = clx(
|
const inputClass = clx(
|
||||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||||
@@ -27,6 +30,15 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
props.className
|
props.className
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.variant === 'search' && props.onChangeSearchFinal) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
props.onChangeSearchFinal?.(search)
|
||||||
|
}, 1000)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [search, props])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -35,7 +47,10 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className='w-full relative mt-1'>
|
<div className='w-full relative mt-1'>
|
||||||
<input {...props} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
<input onChange={(e) => {
|
||||||
|
setSearch(e.target.value)
|
||||||
|
props.onChange?.(e)
|
||||||
|
}} {...props} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
||||||
|
|
||||||
{
|
{
|
||||||
props.type === 'password' &&
|
props.type === 'password' &&
|
||||||
@@ -46,6 +61,13 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
props.variant === 'search' &&
|
props.variant === 'search' &&
|
||||||
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
props.error_text &&
|
||||||
|
<Error
|
||||||
|
errorText={props.error_text}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import Logo from '../assets/images/logo_orig.svg'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const PageLoading: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex bg-white w-fit mx-auto flex-col gap-4 items-center'>
|
||||||
|
<img src={Logo} alt='logo' className='w-28' />
|
||||||
|
<div className='text-xs text-gray-700'>
|
||||||
|
{t('loading')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageLoading
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PaginationProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Pagination: React.FC<PaginationProps> = ({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onPageChange,
|
||||||
|
}) => {
|
||||||
|
const getPageNumbers = () => {
|
||||||
|
const pageNumbers: (number | string)[] = [];
|
||||||
|
const maxVisiblePages = 5; // تعداد حداکثری صفحات قابل مشاهده
|
||||||
|
|
||||||
|
if (totalPages <= maxVisiblePages) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentPage > 3) {
|
||||||
|
pageNumbers.push(1);
|
||||||
|
if (currentPage > 4) {
|
||||||
|
pageNumbers.push("...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Math.max(2, currentPage - 1);
|
||||||
|
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pageNumbers.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPage < totalPages - 2) {
|
||||||
|
if (currentPage < totalPages - 3) {
|
||||||
|
pageNumbers.push("...");
|
||||||
|
}
|
||||||
|
pageNumbers.push(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageNumbers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageNumbers = getPageNumbers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 text-xs justify-center items-center space-x-2 mt-4">
|
||||||
|
{/* دکمه قبلی */}
|
||||||
|
{/* <button
|
||||||
|
className={`px-3 py-1 rounded-md ${currentPage === 1
|
||||||
|
? "text-gray-400 cursor-not-allowed"
|
||||||
|
: "text-blue-600 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
قبلی
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
{/* شماره صفحات */}
|
||||||
|
{pageNumbers.map((page, index) =>
|
||||||
|
typeof page === "number" ? (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={`size-8 rounded-md ${currentPage === page
|
||||||
|
? "bg-primary text-white"
|
||||||
|
: "text-primary bg-[#EAECF5] hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => onPageChange(page)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span key={index} className="px-3 py-1 text-gray-500">
|
||||||
|
{page}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* دکمه بعدی */}
|
||||||
|
{/* <button
|
||||||
|
className={`px-3 py-1 rounded-md ${currentPage === totalPages
|
||||||
|
? "text-gray-400 cursor-not-allowed"
|
||||||
|
: "text-blue-600 hover:bg-gray-100"
|
||||||
|
}`}
|
||||||
|
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
بعدی
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Pagination;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FC, SelectHTMLAttributes } from 'react'
|
import { FC, SelectHTMLAttributes } from 'react'
|
||||||
import { clx } from '../helpers/utils'
|
import { clx } from '../helpers/utils'
|
||||||
import { ArrowDown2 } from 'iconsax-react'
|
import { ArrowDown2 } from 'iconsax-react'
|
||||||
|
import Error from './Error'
|
||||||
|
|
||||||
export type ItemsSelectType = {
|
export type ItemsSelectType = {
|
||||||
value: string,
|
value: string,
|
||||||
@@ -48,9 +49,9 @@ const Select: FC<Props> = (props: Props) => {
|
|||||||
)} />
|
)} />
|
||||||
{
|
{
|
||||||
props.error_text && props.error_text !== '' ?
|
props.error_text && props.error_text !== '' ?
|
||||||
<div className='text-xs text-right text-red-600 mt-2 mr-2 font-medium'>
|
<Error
|
||||||
{props.error_text}
|
errorText={props.error_text}
|
||||||
</div>
|
/>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BASE_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
axiosInstance.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
localStorage.removeItem(import.meta.env.VITE_TOKEN_NAME);
|
||||||
|
window.location.href = "/auth/login";
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
axiosInstance.interceptors.request.use(async function (config) {
|
||||||
|
const token = localStorage.getItem(import.meta.env.VITE_TOKEN_NAME)
|
||||||
|
? localStorage.getItem(import.meta.env.VITE_TOKEN_NAME)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
config.headers.Authorization = "Bearer " + token;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default axiosInstance;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export function isEmail(input: string): boolean {
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
return emailRegex.test(input);
|
||||||
|
}
|
||||||
+13
-3
@@ -5,6 +5,16 @@ export type XOR<T, U> = T | U extends object
|
|||||||
: T | U;
|
: T | U;
|
||||||
|
|
||||||
export type ItemsSelectType = {
|
export type ItemsSelectType = {
|
||||||
value: any,
|
value: string;
|
||||||
label: string,
|
label: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type ErrorType = Error & {
|
||||||
|
response?: {
|
||||||
|
data?: {
|
||||||
|
error: {
|
||||||
|
message: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -145,3 +145,8 @@ tbody tr {
|
|||||||
.ql-toolbar:first-child {
|
.ql-toolbar:first-child {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Toastify__toast {
|
||||||
|
direction: rtl;
|
||||||
|
font-family: "irancell" !important;
|
||||||
|
}
|
||||||
|
|||||||
+6
-2
@@ -151,7 +151,9 @@
|
|||||||
"status_category": "وضعیت دسته بندی",
|
"status_category": "وضعیت دسته بندی",
|
||||||
"title_category": "عنوان دسته بندی",
|
"title_category": "عنوان دسته بندی",
|
||||||
"category_parent_2": "دسته بندی مادر",
|
"category_parent_2": "دسته بندی مادر",
|
||||||
"icon_serice_category": "آیکون سرویس مورد نظر را آپلود کنید"
|
"icon_serice_category": "آیکون سرویس مورد نظر را آپلود کنید",
|
||||||
|
"active": "فعال",
|
||||||
|
"deactive": "غیرفعال"
|
||||||
},
|
},
|
||||||
"save": "ذخیره",
|
"save": "ذخیره",
|
||||||
"search": "جستجو",
|
"search": "جستجو",
|
||||||
@@ -481,5 +483,7 @@
|
|||||||
"email": "ایمیل",
|
"email": "ایمیل",
|
||||||
"subject": "موضوع",
|
"subject": "موضوع",
|
||||||
"date": "تاریخ"
|
"date": "تاریخ"
|
||||||
}
|
},
|
||||||
|
"loading": "در حال بارگذاری...",
|
||||||
|
"success": "عملیات با موفقیت انجام شد."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import LoginStep3 from './components/LoginStep3'
|
|||||||
|
|
||||||
const Login: FC = () => {
|
const Login: FC = () => {
|
||||||
|
|
||||||
const { stepLogin } = useAuthStore()
|
const { stepLogin, phone, email } = useAuthStore()
|
||||||
|
|
||||||
|
console.log(phone, email, stepLogin);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -23,11 +25,13 @@ const Login: FC = () => {
|
|||||||
stepLogin === 1 ?
|
stepLogin === 1 ?
|
||||||
<LoginStep1 />
|
<LoginStep1 />
|
||||||
:
|
:
|
||||||
stepLogin === 2 ?
|
stepLogin === 2 && !!phone ?
|
||||||
<LoginStep2 />
|
<LoginStep2 />
|
||||||
: stepLogin === 3 ?
|
: stepLogin === 2 && !!email ?
|
||||||
<LoginStep3 />
|
<LoginStep3 />
|
||||||
: null
|
: stepLogin === 3 ?
|
||||||
|
<LoginStep3 />
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+13
-53
@@ -2,20 +2,23 @@ import { FC } from 'react'
|
|||||||
import LogoImage from '../../assets/images/logo.svg'
|
import LogoImage from '../../assets/images/logo.svg'
|
||||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Input from '../../components/Input'
|
|
||||||
import DatePickerComponent from '../../components/DatePicker'
|
|
||||||
import Button from '../../components/Button'
|
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Pages } from '../../config/Pages'
|
import { Pages } from '../../config/Pages'
|
||||||
|
import RegisterStep1 from './components/RegisterStep1'
|
||||||
|
import { useAuthStore } from './store/AuthStore'
|
||||||
|
import RegisterStep2 from './components/RegisterStep2'
|
||||||
|
|
||||||
|
|
||||||
const Register: FC = () => {
|
const Register: FC = () => {
|
||||||
|
|
||||||
|
const { stepLogin } = useAuthStore()
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
<div className='flex-1 flex flex-col justify-center min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||||
<div className=''>
|
<div className=''>
|
||||||
<img src={LogoSmallImage} className='w-8' />
|
<img src={LogoSmallImage} className='w-8' />
|
||||||
<div className='mt-5'>
|
<div className='mt-5'>
|
||||||
@@ -27,55 +30,12 @@ const Register: FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className='mt-5 text-sm'>
|
{
|
||||||
{t('auth.user_info')}
|
stepLogin === 1 ?
|
||||||
</div> */}
|
<RegisterStep1 />
|
||||||
|
:
|
||||||
<div className='mt-5 rowTwoInput'>
|
<RegisterStep2 />
|
||||||
<Input
|
}
|
||||||
label={t('auth.name')}
|
|
||||||
placeholder={t('auth.enter_name')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label={t('auth.family')}
|
|
||||||
placeholder={t('auth.enter_family')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className='mt-4 min-w-full rowTwoInput'>
|
|
||||||
<DatePickerComponent label={t('auth.dateofbirth')} onChange={(date: string) => console.log(date)} placeholder={t('auth.select_date')} />
|
|
||||||
|
|
||||||
<Input
|
|
||||||
label={t('auth.national_code')}
|
|
||||||
placeholder={t('auth.national_code_enter')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-4 rowTwoInput'>
|
|
||||||
<Input
|
|
||||||
label={t('auth.email')}
|
|
||||||
placeholder={t('auth.enter_email')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label={t('auth.phonen_number')}
|
|
||||||
placeholder={t('auth.enter_phone_number')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-4 rowTwoInput'>
|
|
||||||
<Input
|
|
||||||
label={t('auth.password')}
|
|
||||||
placeholder={t('auth.enter_password')}
|
|
||||||
type='password'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
label={t('auth.next')}
|
|
||||||
className='mt-8'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
||||||
<p className='text-description'>
|
<p className='text-description'>
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import * as Yup from 'yup'
|
|||||||
import { useAuthStore } from '../store/AuthStore'
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
import Error from '../../../components/Error'
|
import Error from '../../../components/Error'
|
||||||
import Button from '../../../components/Button'
|
import Button from '../../../components/Button'
|
||||||
import { Link } from 'react-router-dom'
|
import { isEmail } from '../../../config/func'
|
||||||
import { Pages } from '../../../config/Pages'
|
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
const LoginStep1: FC = () => {
|
const LoginStep1: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { setPhone, phone, setStepLogin } = useAuthStore()
|
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore()
|
||||||
|
const loginWithOtp = useLoginWithOtp()
|
||||||
|
const checkHasAccount = useCheckHasAccount()
|
||||||
|
|
||||||
const formik = useFormik<LoginType>({
|
const formik = useFormik<LoginType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -24,8 +28,28 @@ const LoginStep1: FC = () => {
|
|||||||
.required(t('errors.required'))
|
.required(t('errors.required'))
|
||||||
}),
|
}),
|
||||||
onSubmit(values) {
|
onSubmit(values) {
|
||||||
setPhone(values.phone_email)
|
if (isEmail(values.phone_email)) {
|
||||||
setStepLogin(2)
|
setEmail(values.phone_email)
|
||||||
|
checkHasAccount.mutate({ email: values.phone_email }, {
|
||||||
|
onSuccess() {
|
||||||
|
setStepLogin(2)
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setPhone(values.phone_email)
|
||||||
|
loginWithOtp.mutate({ phone: values.phone_email }, {
|
||||||
|
onSuccess() {
|
||||||
|
setStepLogin(2)
|
||||||
|
toast.success(t('auth.otp_sent'))
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,16 +89,9 @@ const LoginStep1: FC = () => {
|
|||||||
label={t('auth.next')}
|
label={t('auth.next')}
|
||||||
className='mt-8'
|
className='mt-8'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
|
||||||
<p className='text-description'>
|
|
||||||
{t('auth.have_account')}
|
|
||||||
</p>
|
|
||||||
<Link to={Pages.auth.register}>
|
|
||||||
<div>{t('auth.register')}</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,49 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { LoginType } from '../types/AuthTypes'
|
import { OtpVerifyType } from '../types/AuthTypes'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useAuthStore } from '../store/AuthStore'
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
import Button from '../../../components/Button'
|
import Button from '../../../components/Button'
|
||||||
import OTPInput from 'react-otp-input'
|
import OTPInput from 'react-otp-input'
|
||||||
import { useCountDown } from '../../../hooks/useCountDown'
|
import { useCountDown } from '../../../hooks/useCountDown'
|
||||||
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
// import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||||
import { Pages } from '../../../config/Pages'
|
import { Pages } from '../../../config/Pages'
|
||||||
|
import { useOtpVerify } from '../hooks/useAuthData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
const LoginStep2: FC = () => {
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { setPhone, phone, setStepLogin } = useAuthStore()
|
const { phone, setStepLogin } = useAuthStore()
|
||||||
const [otpValue, setOtpValue] = useState<string>()
|
|
||||||
const { value } = useCountDown(2, true)
|
const { value } = useCountDown(2, true)
|
||||||
|
const otpVerify = useOtpVerify()
|
||||||
|
|
||||||
const formik = useFormik<LoginType>({
|
const formik = useFormik<OtpVerifyType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
phone_email: phone,
|
phone: phone,
|
||||||
|
code: ''
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
phone_email: Yup.string()
|
code: Yup.string()
|
||||||
.required(t('errors.required'))
|
.required(t('errors.required'))
|
||||||
}),
|
}),
|
||||||
onSubmit(values) {
|
onSubmit(values) {
|
||||||
setPhone(values.phone_email)
|
const params: OtpVerifyType = {
|
||||||
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, 'token')
|
phone: phone,
|
||||||
window.location.href = Pages.dashboard
|
code: values.code
|
||||||
|
}
|
||||||
|
otpVerify.mutate(params, {
|
||||||
|
onSuccess(data) {
|
||||||
|
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, data?.data?.accessToken?.token)
|
||||||
|
localStorage.setItem(import.meta.env.VITE_REFRESH_TOKEN_NAME, data?.data?.refreshToken?.token)
|
||||||
|
window.location.href = Pages.dashboard
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -59,14 +74,19 @@ const LoginStep2: FC = () => {
|
|||||||
<div className='mt-2 w-full flex justify-center dltr otp'>
|
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||||
<OTPInput
|
<OTPInput
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
value={otpValue}
|
value={formik.values.code}
|
||||||
numInputs={5}
|
numInputs={5}
|
||||||
inputType='tel'
|
inputType='tel'
|
||||||
onChange={(otp: string) => setOtpValue(otp)}
|
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||||
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.code && formik.errors.code &&
|
||||||
|
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className='flex mt-4 justify-end'>
|
<div className='flex mt-4 justify-end'>
|
||||||
<div className='flex gap-1 text-description text-xs'>
|
<div className='flex gap-1 text-description text-xs'>
|
||||||
<div>{value}</div>
|
<div>{value}</div>
|
||||||
@@ -81,14 +101,15 @@ const LoginStep2: FC = () => {
|
|||||||
label={t('auth.login')}
|
label={t('auth.login')}
|
||||||
className='mt-8'
|
className='mt-8'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={otpVerify.isPending}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
{/* <div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||||
<p className='text-description'>
|
<p className='text-description'>
|
||||||
{t('auth.login_with_password')}
|
{t('auth.login_with_password')}
|
||||||
</p>
|
</p>
|
||||||
<img src={ArrowLeftIcon} className='w-5' />
|
<img src={ArrowLeftIcon} className='w-5' />
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Input from '../../../components/Input'
|
import Input from '../../../components/Input'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { LoginPasswordType } from '../types/AuthTypes'
|
import { LoginWithPasswordType } from '../types/AuthTypes'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { useAuthStore } from '../store/AuthStore'
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
@@ -10,24 +10,36 @@ import Button from '../../../components/Button'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Pages } from '../../../config/Pages'
|
import { Pages } from '../../../config/Pages'
|
||||||
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||||
|
import { useLoginWithPassword } from '../hooks/useAuthData'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
const LoginStep3: FC = () => {
|
const LoginStep3: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { setStepLogin } = useAuthStore()
|
const { setStepLogin, email } = useAuthStore()
|
||||||
|
const loginWithPassword = useLoginWithPassword()
|
||||||
|
|
||||||
const formik = useFormik<LoginPasswordType>({
|
const formik = useFormik<LoginWithPasswordType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
password: '',
|
password: '',
|
||||||
|
email: email
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
phone_email: Yup.string()
|
password: Yup.string()
|
||||||
.required(t('errors.required'))
|
.required(t('errors.required'))
|
||||||
}),
|
}),
|
||||||
onSubmit(values) {
|
onSubmit(values) {
|
||||||
console.log(values)
|
loginWithPassword.mutate(values, {
|
||||||
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, 'token')
|
onSuccess(data) {
|
||||||
window.location.href = Pages.dashboard
|
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, data?.data?.accessToken?.token)
|
||||||
|
localStorage.setItem(import.meta.env.VITE_REFRESH_TOKEN_NAME, data?.data?.refreshToken?.token)
|
||||||
|
window.location.href = Pages.dashboard
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -51,6 +63,7 @@ const LoginStep3: FC = () => {
|
|||||||
name='password'
|
name='password'
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.password}
|
value={formik.values.password}
|
||||||
|
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { CheckHasAccountPhoneType } from '../types/AuthTypes'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import Error from '../../../components/Error'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { useCheckHasAccountRegister, useLoginWithOtp } from '../hooks/useAuthData'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const RegisterStep1: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { setPhone, setStepLogin } = useAuthStore()
|
||||||
|
const loginWithOtp = useLoginWithOtp()
|
||||||
|
const checkHasAccount = useCheckHasAccountRegister()
|
||||||
|
|
||||||
|
const formik = useFormik<CheckHasAccountPhoneType>({
|
||||||
|
initialValues: {
|
||||||
|
phone: '',
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
phone: Yup.string()
|
||||||
|
.required(t('errors.required'))
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
checkHasAccount.mutate(values, {
|
||||||
|
onSuccess() {
|
||||||
|
setPhone(values.phone)
|
||||||
|
setStepLogin(2)
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex-1'>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='mt-16'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.mobile_number')}
|
||||||
|
placeholder={t('auth.enter_mobile_number')}
|
||||||
|
type='tel'
|
||||||
|
className='text-right'
|
||||||
|
name='phone'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.phone}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
formik.touched.phone && formik.errors.phone &&
|
||||||
|
<Error
|
||||||
|
errorText={formik.errors.phone}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.next')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterStep1
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { FC, Fragment } from 'react'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { RegisterType } from '../types/AuthTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useAuthStore } from '../store/AuthStore'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import DatePickerComponent from '../../../components/DatePicker'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { useRegister } from '../hooks/useAuthData'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { Pages } from '../../../config/Pages'
|
||||||
|
|
||||||
|
const RegisterStep2: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const { phone } = useAuthStore()
|
||||||
|
const register = useRegister()
|
||||||
|
|
||||||
|
const formik = useFormik<RegisterType>({
|
||||||
|
initialValues: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
birthDate: '',
|
||||||
|
code: 0,
|
||||||
|
nationalCode: '',
|
||||||
|
password: '',
|
||||||
|
phone: phone,
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
firstName: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
lastName: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
birthDate: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
code: Yup.number()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
nationalCode: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
password: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
phone: Yup.string()
|
||||||
|
.required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit(values) {
|
||||||
|
register.mutate(values, {
|
||||||
|
onSuccess(data) {
|
||||||
|
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, data?.data?.accessToken?.token)
|
||||||
|
localStorage.setItem(import.meta.env.VITE_REFRESH_TOKEN_NAME, data?.data?.refreshToken?.token)
|
||||||
|
window.location.href = Pages.dashboard
|
||||||
|
},
|
||||||
|
onError(error: ErrorType) {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(formik.errors);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className='mt-5 rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.name')}
|
||||||
|
placeholder={t('auth.enter_name')}
|
||||||
|
name='firstName'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={t('auth.family')}
|
||||||
|
placeholder={t('auth.enter_family')}
|
||||||
|
name='lastName'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='mt-4 min-w-full rowTwoInput'>
|
||||||
|
<DatePickerComponent
|
||||||
|
label={t('auth.dateofbirth')}
|
||||||
|
onChange={(date: string) => formik.setFieldValue('birthDate', date)}
|
||||||
|
placeholder={t('auth.select_date')}
|
||||||
|
error_text={formik.touched.birthDate && formik.errors.birthDate ? formik.errors.birthDate : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label={t('auth.national_code')}
|
||||||
|
placeholder={t('auth.national_code_enter')}
|
||||||
|
name='nationalCode'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.nationalCode && formik.errors.nationalCode ? formik.errors.nationalCode : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.password')}
|
||||||
|
placeholder={t('auth.enter_password')}
|
||||||
|
type='password'
|
||||||
|
name='password'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={t('auth.phonen_number')}
|
||||||
|
placeholder={t('auth.enter_phone_number')}
|
||||||
|
readOnly
|
||||||
|
value={phone}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 rowTwoInput'>
|
||||||
|
<Input
|
||||||
|
label={t('auth.verify_code')}
|
||||||
|
placeholder={t('auth.enter_verify_code')}
|
||||||
|
type='tel'
|
||||||
|
name='code'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.code && formik.errors.code ? formik.errors.code : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={t('auth.next')}
|
||||||
|
className='mt-8'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={register.isPending}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterStep2
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import * as api from '../service/AuthService'
|
||||||
|
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
|
||||||
|
|
||||||
|
export const useLoginWithPassword = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: LoginWithPasswordType) => api.loginWithPassword(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLoginWithOtp = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: LoginWithOtpType) => api.loginWithOtp(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOtpVerify = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: OtpVerifyType) => api.otpVerify(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCheckHasAccount = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CheckHasAccountType) => api.checkHasAccount(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCheckHasAccountRegister = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CheckHasAccountPhoneType) => api.checkHasAccountRegister(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegister = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: RegisterType) => api.register(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import {
|
||||||
|
CheckHasAccountPhoneType,
|
||||||
|
CheckHasAccountType,
|
||||||
|
LoginWithOtpType,
|
||||||
|
LoginWithPasswordType,
|
||||||
|
OtpVerifyType,
|
||||||
|
RegisterType,
|
||||||
|
} from "../types/AuthTypes";
|
||||||
|
|
||||||
|
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||||
|
const { data } = await axios.post(`/auth/login/password/admin`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginWithOtp = async (params: LoginWithOtpType) => {
|
||||||
|
const { data } = await axios.post(`/auth/otp/send`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const otpVerify = async (params: OtpVerifyType) => {
|
||||||
|
const { data } = await axios.post(`/auth/otp/verify/admin`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
export const checkHasAccount = async (params: CheckHasAccountType) => {
|
||||||
|
const { data } = await axios.post(`/auth/check`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkHasAccountRegister = async (
|
||||||
|
params: CheckHasAccountPhoneType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.post(`/auth/register/initiate`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
export const register = async (params: RegisterType) => {
|
||||||
|
const { data } = await axios.post(`/auth/register/complete`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -6,6 +6,10 @@ export const useAuthStore = create<AuthStoreType>((set) => ({
|
|||||||
setPhone(value) {
|
setPhone(value) {
|
||||||
set({ phone: value });
|
set({ phone: value });
|
||||||
},
|
},
|
||||||
|
email: "",
|
||||||
|
setEmail(value) {
|
||||||
|
set({ email: value });
|
||||||
|
},
|
||||||
stepLogin: 1,
|
stepLogin: 1,
|
||||||
setStepLogin(value) {
|
setStepLogin(value) {
|
||||||
set({ stepLogin: value });
|
set({ stepLogin: value });
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { XOR } from "../../../helpers/types";
|
||||||
|
|
||||||
export type LoginType = {
|
export type LoginType = {
|
||||||
phone_email: string;
|
phone_email: string;
|
||||||
};
|
};
|
||||||
@@ -8,6 +10,42 @@ export type LoginPasswordType = {
|
|||||||
export type AuthStoreType = {
|
export type AuthStoreType = {
|
||||||
phone: string;
|
phone: string;
|
||||||
setPhone: (value: string) => void;
|
setPhone: (value: string) => void;
|
||||||
|
email: string;
|
||||||
|
setEmail: (value: string) => void;
|
||||||
stepLogin: number;
|
stepLogin: number;
|
||||||
setStepLogin: (value: number) => void;
|
setStepLogin: (value: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginWithPasswordType = XOR<
|
||||||
|
{ email: string },
|
||||||
|
{ phone: number }
|
||||||
|
> & {
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginWithOtpType = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OtpVerifyType = {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckHasAccountType = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CheckHasAccountPhoneType = {
|
||||||
|
phone: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterType = {
|
||||||
|
phone: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
birthDate: string;
|
||||||
|
nationalCode: string;
|
||||||
|
password: string;
|
||||||
|
code: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import { FC } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Select from '../../components/Select'
|
import Select from '../../components/Select'
|
||||||
import Input from '../../components/Input'
|
import Input from '../../components/Input'
|
||||||
import Td from '../../components/Td'
|
import Td from '../../components/Td'
|
||||||
import SwitchComponent from '../../components/Switch'
|
import CreateCategory from './components/CreateCategory'
|
||||||
import UploadBoxDraggble from '../../components/UploadBoxDraggble'
|
import PageLoading from '../../components/PageLoading'
|
||||||
import Button from '../../components/Button'
|
import { ServiceCategoryType } from './types/ServiceTypes'
|
||||||
import { TickCircle } from 'iconsax-react'
|
import { useGetAllCategory } from './hooks/useServiceData'
|
||||||
|
import StatusCategory from './components/StatusCategory'
|
||||||
|
import Pagination from '../../components/Pagination'
|
||||||
|
|
||||||
|
|
||||||
const Category: FC = () => {
|
const Category: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
|
const [search, setSearch] = useState<string>('')
|
||||||
|
const [page, setPage] = useState<number>(1)
|
||||||
|
const [isActive, setIsActive] = useState<'active' | 'deactive' | ''>('')
|
||||||
|
const getCategory = useGetAllCategory(page, search, isActive)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-4'>
|
<div className='mt-4'>
|
||||||
@@ -24,11 +31,12 @@ const Category: FC = () => {
|
|||||||
<div className='w-32'>
|
<div className='w-32'>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={[
|
||||||
{ label: 'All', value: 'all' },
|
{ label: t('all'), value: '' },
|
||||||
{ label: 'Active', value: 'active' },
|
{ label: t('service.active'), value: 'active' },
|
||||||
{ label: 'Inactive', value: 'inactive' },
|
{ label: t('service.deactive'), value: 'deactive' },
|
||||||
]}
|
]}
|
||||||
placeholder={t('service.category')}
|
placeholder={t('status')}
|
||||||
|
onChange={(e) => setIsActive(e.target.value as 'active' | 'deactive' | '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,98 +45,70 @@ const Category: FC = () => {
|
|||||||
variant='search'
|
variant='search'
|
||||||
placeholder={t('search')}
|
placeholder={t('search')}
|
||||||
className='bg-white'
|
className='bg-white'
|
||||||
|
onChangeSearchFinal={(value) => setSearch(value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex-1 mt-8 bg-white py-2 xl:px-10 px-5 rounded-3xl'>
|
<div className='flex-1 mt-8 bg-white py-2 xl:px-10 px-5 rounded-3xl'>
|
||||||
<div className='relative overflow-x-auto rounded-3xl w-full'>
|
{
|
||||||
<table className='w-full text-sm '>
|
getCategory.isPending ?
|
||||||
<thead className='thead'>
|
<div className='mt-4'>
|
||||||
<tr>
|
<PageLoading />
|
||||||
<Td text='' />
|
</div>
|
||||||
<Td text={t('service.title')} />
|
:
|
||||||
<Td text={t('service.parent_category')} />
|
<div className='relative overflow-x-auto rounded-3xl w-full'>
|
||||||
<Td text={t('service.count_sub_category')} />
|
<table className='w-full text-sm '>
|
||||||
<Td text={t('service.count_service')} />
|
<thead className='thead'>
|
||||||
<Td text={t('ticket.status')} />
|
<tr>
|
||||||
<Td text={''} />
|
<Td text='' />
|
||||||
</tr>
|
<Td text={t('service.title')} />
|
||||||
</thead>
|
<Td text={t('service.parent_category')} />
|
||||||
<tbody>
|
<Td text={t('service.count_sub_category')} />
|
||||||
<tr className='tr'>
|
<Td text={t('service.count_service')} />
|
||||||
<Td text=''>
|
<Td text={t('ticket.status')} />
|
||||||
<div className='size-10 rounded-lg bg-red-100'></div>
|
<Td text={''} />
|
||||||
</Td>
|
</tr>
|
||||||
<Td text={'کسب و کار'} />
|
</thead>
|
||||||
<Td text={'-'} />
|
<tbody>
|
||||||
<Td text={'2'} />
|
{
|
||||||
<Td text={'14'} />
|
getCategory.data?.data?.categories?.map((item: ServiceCategoryType) => {
|
||||||
<Td text={''}>
|
return (
|
||||||
<SwitchComponent
|
<tr key={item.id} className='tr'>
|
||||||
active
|
<Td text=''>
|
||||||
onChange={() => null}
|
<img src={item.icon} className='size-10 rounded-lg' />
|
||||||
/>
|
</Td>
|
||||||
</Td>
|
<Td text={item.title} />
|
||||||
<Td text={''} />
|
<Td text={item.parent ? item.parent.title : '-'} />
|
||||||
</tr>
|
<Td text={item.childrenCount + ''} />
|
||||||
</tbody>
|
<Td text={item.servicesCount + ''} />
|
||||||
</table>
|
<Td text={''}>
|
||||||
</div>
|
<StatusCategory
|
||||||
|
defaultActive={item.isActive}
|
||||||
|
id={item.id}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td text={''} />
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className='flex justify-end pb-3'>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={getCategory.data?.data?.pager?.totalPages}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='bg-white p-8 w-sidebar hidden xl:block rounded-3xl overflow-hidden relative'>
|
<CreateCategory />
|
||||||
<div>
|
|
||||||
{t('service.add_category')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8 flex justify-between'>
|
|
||||||
<div className='text-sm'>
|
|
||||||
{t('service.status_category')}
|
|
||||||
</div>
|
|
||||||
<div className='flex'>
|
|
||||||
<SwitchComponent active onChange={() => { }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-8'>
|
|
||||||
<Input
|
|
||||||
label={t('service.title_category')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Select
|
|
||||||
label={t('service.category_parent_2')}
|
|
||||||
items={[
|
|
||||||
{ label: 'All', value: 'all' },
|
|
||||||
{ label: 'Active', value: 'active' },
|
|
||||||
{ label: 'Inactive', value: 'inactive' },
|
|
||||||
]}
|
|
||||||
placeholder={t('select')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-6'>
|
|
||||||
<UploadBoxDraggble
|
|
||||||
label={t('service.icon_serice_category')}
|
|
||||||
onChange={() => null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex flex-1 justify-end items-end mt-8'>
|
|
||||||
<Button
|
|
||||||
className='w-fit px-5'
|
|
||||||
>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<TickCircle
|
|
||||||
className='size-5'
|
|
||||||
color='white'
|
|
||||||
/>
|
|
||||||
<div>{t('save')}</div>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import SwitchComponent from '../../../components/Switch'
|
||||||
|
import Input from '../../../components/Input'
|
||||||
|
import Select from '../../../components/Select'
|
||||||
|
import UploadBoxDraggble from '../../../components/UploadBoxDraggble'
|
||||||
|
import Button from '../../../components/Button'
|
||||||
|
import { TickCircle } from 'iconsax-react'
|
||||||
|
import { useCreateServiceCategory, useGetCategoryParents } from '../hooks/useServiceData'
|
||||||
|
import { CreateServiceCategoryType, ServiceCategoryType } from '../types/ServiceTypes'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ErrorType } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const CreateCategory: FC = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const getCategory = useGetCategoryParents()
|
||||||
|
const createCategory = useCreateServiceCategory()
|
||||||
|
|
||||||
|
const formik = useFormik<CreateServiceCategoryType>({
|
||||||
|
initialValues: {
|
||||||
|
title: '',
|
||||||
|
isActive: true,
|
||||||
|
parentId: ''
|
||||||
|
},
|
||||||
|
validationSchema: Yup.object({
|
||||||
|
title: Yup.string().required(t('errors.required')),
|
||||||
|
}),
|
||||||
|
onSubmit: (values) => {
|
||||||
|
const params = {
|
||||||
|
...values,
|
||||||
|
icon: 'https://picsum.photos/200/300',
|
||||||
|
parentId: values.parentId ? values.parentId : undefined
|
||||||
|
}
|
||||||
|
createCategory.mutate(params, {
|
||||||
|
onSuccess: () => {
|
||||||
|
formik.resetForm()
|
||||||
|
getCategory.refetch()
|
||||||
|
toast.success(t('success'))
|
||||||
|
},
|
||||||
|
onError: (error: ErrorType) => {
|
||||||
|
toast.error(error?.response?.data?.error?.message[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='bg-white p-8 w-sidebar hidden xl:block rounded-3xl overflow-hidden relative'>
|
||||||
|
<div>
|
||||||
|
{t('service.add_category')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8 flex justify-between'>
|
||||||
|
<div className='text-sm'>
|
||||||
|
{t('service.status_category')}
|
||||||
|
</div>
|
||||||
|
<div className='flex'>
|
||||||
|
<SwitchComponent
|
||||||
|
active={formik.values.isActive}
|
||||||
|
onChange={(value) => formik.setFieldValue('isActive', value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-8'>
|
||||||
|
<Input
|
||||||
|
label={t('service.title_category')}
|
||||||
|
value={formik.values.title}
|
||||||
|
name='title'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
getCategory.data &&
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Select
|
||||||
|
label={t('service.category_parent_2')}
|
||||||
|
items={getCategory.data?.data?.categories?.map((item: ServiceCategoryType) => {
|
||||||
|
return {
|
||||||
|
label: item.title,
|
||||||
|
value: item.id
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
placeholder={t('select')}
|
||||||
|
name='parentId'
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error_text={formik.touched.parentId && formik.errors.parentId ? formik.errors.parentId : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<div className='mt-6'>
|
||||||
|
<UploadBoxDraggble
|
||||||
|
label={t('service.icon_serice_category')}
|
||||||
|
onChange={() => null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-1 justify-end items-end mt-8'>
|
||||||
|
<Button
|
||||||
|
className='w-fit px-5'
|
||||||
|
onClick={() => formik.handleSubmit()}
|
||||||
|
isLoading={createCategory.isPending}
|
||||||
|
>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<TickCircle
|
||||||
|
className='size-5'
|
||||||
|
color='white'
|
||||||
|
/>
|
||||||
|
<div>{t('save')}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateCategory
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import SwitchComponent from '../../../components/Switch'
|
||||||
|
import { useChangeStatusCategory } from '../hooks/useServiceData'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultActive: boolean,
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusCategory: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState<boolean>(props.defaultActive)
|
||||||
|
const toggleStatus = useChangeStatusCategory()
|
||||||
|
|
||||||
|
const handleChange = (value: boolean) => {
|
||||||
|
setIsActive(value)
|
||||||
|
toggleStatus.mutate({ id: props.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SwitchComponent
|
||||||
|
active={isActive}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StatusCategory
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import * as api from "../service/ServiceService";
|
||||||
|
import {
|
||||||
|
CreateServiceCategoryType,
|
||||||
|
ToggleStatusCategoryType,
|
||||||
|
} from "../types/ServiceTypes";
|
||||||
|
|
||||||
|
export const useGetCategoryParents = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["service-category"],
|
||||||
|
queryFn: api.getCategoryParent,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetAllCategory = (
|
||||||
|
page: number,
|
||||||
|
search: string,
|
||||||
|
isActive: "active" | "deactive" | ""
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["service-category-all", page, search, isActive],
|
||||||
|
queryFn: () => api.getAllCategory(page, search, isActive),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateServiceCategory = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: CreateServiceCategoryType) =>
|
||||||
|
api.createServiceCategory(variables),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries({
|
||||||
|
queryKey: ["service-category-all"],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useChangeStatusCategory = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (variables: ToggleStatusCategoryType) =>
|
||||||
|
api.changeStatusCategory(variables),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import axios from "../../../config/axios";
|
||||||
|
import {
|
||||||
|
CreateServiceCategoryType,
|
||||||
|
ToggleStatusCategoryType,
|
||||||
|
} from "../types/ServiceTypes";
|
||||||
|
|
||||||
|
export const getCategoryParent = async () => {
|
||||||
|
const { data } = await axios.get(`/services/category`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllCategory = async (
|
||||||
|
page: number,
|
||||||
|
search: string,
|
||||||
|
isActive: "active" | "deactive" | ""
|
||||||
|
) => {
|
||||||
|
let query = `?page=${page}&q=${search}`;
|
||||||
|
if (isActive) {
|
||||||
|
query += `&isActive=${isActive === "active" ? "1" : "0"}`;
|
||||||
|
}
|
||||||
|
const { data } = await axios.get(`/services/category-list${query}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createServiceCategory = async (
|
||||||
|
params: CreateServiceCategoryType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.post(`/services/category`, params);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const changeStatusCategory = async (
|
||||||
|
params: ToggleStatusCategoryType
|
||||||
|
) => {
|
||||||
|
const { data } = await axios.post(
|
||||||
|
`/services/category/toggle-status/${params.id}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type ServiceCategoryType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
parent: ServiceCategoryType;
|
||||||
|
|
||||||
|
children: ServiceCategoryType[];
|
||||||
|
|
||||||
|
parentId: string;
|
||||||
|
childrenCount: number;
|
||||||
|
servicesCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateServiceCategoryType = {
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
isActive: boolean;
|
||||||
|
parentId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ToggleStatusCategoryType = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
@@ -2,13 +2,11 @@ import { FC } from 'react'
|
|||||||
import { Route, Routes } from 'react-router-dom'
|
import { Route, Routes } from 'react-router-dom'
|
||||||
import { Pages } from '../config/Pages'
|
import { Pages } from '../config/Pages'
|
||||||
import Login from '../pages/auth/Login'
|
import Login from '../pages/auth/Login'
|
||||||
import Register from '../pages/auth/Register'
|
|
||||||
|
|
||||||
const AuthRouter: FC = () => {
|
const AuthRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={Pages.auth.login} element={<Login />} />
|
<Route path={Pages.auth.login} element={<Login />} />
|
||||||
<Route path={Pages.auth.register} element={<Register />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('sidebar.logout')}
|
title={t('sidebar.logout')}
|
||||||
isActive={isActive('logout')}
|
isActive={isActive('logout')}
|
||||||
link={`#`}
|
link={`#`}
|
||||||
|
isLogout
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user