Merge branch 'develop' into feat/user-completion-form
This commit is contained in:
1
.env
Normal file
1
.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
|
||||||
@@ -4,18 +4,18 @@
|
|||||||
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-latest
|
vmImage: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: NodeTool@0
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: '20.x'
|
versionSpec: '20.x'
|
||||||
displayName: 'Install Node.js'
|
displayName: 'Install Node.js'
|
||||||
|
|
||||||
- script: |
|
- script: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
displayName: 'npm install and build'
|
displayName: 'npm install and build'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|||||||
98
package-lock.json
generated
98
package-lock.json
generated
@@ -20,13 +20,18 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-jalali": "^4.0.0-0",
|
"date-fns-jalali": "^4.0.0-0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"@rkheftan/harmony-ui": "^0.1.6",
|
||||||
|
"@types/stylis": "^4.2.7",
|
||||||
"i18next": "^25.3.0",
|
"i18next": "^25.3.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"libphonenumber-js": "^1.12.10",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-country-flag": "^3.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^15.6.0",
|
"react-i18next": "^15.6.0",
|
||||||
|
"react-router-dom": "^7.8.0",
|
||||||
"stylis": "^4.3.6",
|
"stylis": "^4.3.6",
|
||||||
"stylis-plugin-rtl": "^2.1.1"
|
"stylis-plugin-rtl": "^2.1.1"
|
||||||
},
|
},
|
||||||
@@ -35,6 +40,7 @@
|
|||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.0.10",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@types/stylis": "^4.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||||
"@typescript-eslint/parser": "^8.35.1",
|
"@typescript-eslint/parser": "^8.35.1",
|
||||||
"@vitejs/plugin-react": "^4.5.2",
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
@@ -2402,6 +2408,13 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stylis": {
|
||||||
|
"version": "4.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz",
|
||||||
|
"integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.39.1",
|
"version": "8.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||||
@@ -2686,6 +2699,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -3222,18 +3236,10 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/core-util-is": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||||
@@ -3802,6 +3808,36 @@
|
|||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint/node_modules/ajv": {
|
||||||
|
"version": "6.12.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint/node_modules/brace-expansion": {
|
"node_modules/eslint/node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -4491,27 +4527,6 @@
|
|||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ieee754": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
@@ -4705,7 +4720,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/json-schema-typed": {
|
"node_modules/json-schema-typed": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
@@ -4849,6 +4865,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/libphonenumber-js": {
|
||||||
|
"version": "1.12.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz",
|
||||||
|
"integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==",
|
||||||
|
"license": "MIT"
|
||||||
"node_modules/lie": {
|
"node_modules/lie": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||||
@@ -5497,6 +5518,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-country-flag": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.1.1",
|
"version": "19.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||||
@@ -5556,7 +5589,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
|
||||||
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
|
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
"set-cookie-parser": "^2.6.0"
|
"set-cookie-parser": "^2.6.0"
|
||||||
@@ -5579,7 +5611,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
|
||||||
"integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
|
"integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.8.0"
|
"react-router": "7.8.0"
|
||||||
},
|
},
|
||||||
@@ -5834,8 +5865,7 @@
|
|||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/setimmediate": {
|
"node_modules/setimmediate": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
|
|||||||
@@ -18,18 +18,22 @@
|
|||||||
"@mui/stylis-plugin-rtl": "^7.2.0",
|
"@mui/stylis-plugin-rtl": "^7.2.0",
|
||||||
"@mui/x-date-pickers": "^8.9.0",
|
"@mui/x-date-pickers": "^8.9.0",
|
||||||
"@mui/x-date-pickers-pro": "^8.9.0",
|
"@mui/x-date-pickers-pro": "^8.9.0",
|
||||||
"@rkheftan/harmony-ui": "^0.1.5",
|
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns-jalali": "^4.0.0-0",
|
"date-fns-jalali": "^4.0.0-0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"@rkheftan/harmony-ui": "^0.1.6",
|
||||||
|
"@types/stylis": "^4.2.7",
|
||||||
"i18next": "^25.3.0",
|
"i18next": "^25.3.0",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"libphonenumber-js": "^1.12.10",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
"react-country-flag": "^3.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^15.6.0",
|
"react-i18next": "^15.6.0",
|
||||||
|
"react-router-dom": "^7.8.0",
|
||||||
"stylis": "^4.3.6",
|
"stylis": "^4.3.6",
|
||||||
"stylis-plugin-rtl": "^2.1.1"
|
"stylis-plugin-rtl": "^2.1.1"
|
||||||
},
|
},
|
||||||
@@ -38,6 +42,7 @@
|
|||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.0.10",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@types/stylis": "^4.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||||
"@typescript-eslint/parser": "^8.35.1",
|
"@typescript-eslint/parser": "^8.35.1",
|
||||||
"@vitejs/plugin-react": "^4.5.2",
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
|
|||||||
53
public/locales/en/authentication.json
Normal file
53
public/locales/en/authentication.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"loginForm": {
|
||||||
|
"title": "Login/Register",
|
||||||
|
"description": "Please enter your email/password to start",
|
||||||
|
"emailOrPhoneLabel": "Email/Password",
|
||||||
|
"submitButton": "Login/Register",
|
||||||
|
"loginWithGoogle": "Login with google",
|
||||||
|
"emailIsInvalid": "Email is invalid",
|
||||||
|
"phoneNumberIsInvalid": "Phone number is invalid",
|
||||||
|
"thisFieldIsRequired": "This field is requried"
|
||||||
|
},
|
||||||
|
"verify": {
|
||||||
|
"verify": "Verify",
|
||||||
|
"a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.",
|
||||||
|
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.",
|
||||||
|
"a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.",
|
||||||
|
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.",
|
||||||
|
"theVerificationCodeIsIncorrect": "The verification code is incorrect.",
|
||||||
|
"youHaveSuccessfullyLoggedIn": "You have successfully logged in",
|
||||||
|
"youHaveSuccessfullySignedIn": "You have successfully signed in",
|
||||||
|
"resendCodeIn": "Resend code in",
|
||||||
|
"moreMinute": "minute",
|
||||||
|
"resendCode": "Resend code"
|
||||||
|
},
|
||||||
|
"completeSignUp": {
|
||||||
|
"completeSignUp": "Complete Sign Up",
|
||||||
|
"emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "Email {{ email }} has been successfully verified. Please enter your contact number to continue.",
|
||||||
|
"phoneNumber": "Phone number"
|
||||||
|
},
|
||||||
|
"enterPassword": {
|
||||||
|
"loginWithPassword": "Login with password",
|
||||||
|
"enterThePasswordYouSetForYourAccount": "Enter the password you set for your account.",
|
||||||
|
"loginPassword": "Login password",
|
||||||
|
"loginWithOneTimeCode": "Login with one-time code",
|
||||||
|
"iForgotMyPassword": "I forgot my password."
|
||||||
|
},
|
||||||
|
"forgetPassword": {
|
||||||
|
"forgetPassword": "Forget password",
|
||||||
|
"pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "Please enter your mobile number/email to recover your password.",
|
||||||
|
"anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "An email containing a recovery code has been sent to this email address.",
|
||||||
|
"anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "An recovery code has been sent to this phone number.",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"changePassword": "Change password",
|
||||||
|
"createANewPassword": "Create a new password",
|
||||||
|
"newPassword": "New password",
|
||||||
|
"includingANumber": "Including a number",
|
||||||
|
"atLeast8Characters": "At least 8 characters",
|
||||||
|
"containsAnUppercaseAndLowercaseLetter": "Contains an uppercase and lowercase letter",
|
||||||
|
"ContainsASymbol": "Contains the symbol (!@#$%&*^)",
|
||||||
|
"confirmPassword": "Confirm password",
|
||||||
|
"passwordChangedSuccessfully": "Password changed successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,250 @@
|
|||||||
{
|
{
|
||||||
"helloWorld": "hello world"
|
"helloWorld": "hello world",
|
||||||
|
"country": {
|
||||||
|
"afghanistan": "Afghanistan",
|
||||||
|
"aland_islands": "Aland islands",
|
||||||
|
"albania": "Albania",
|
||||||
|
"algeria": "Algeria",
|
||||||
|
"american_samoa": "American samoa",
|
||||||
|
"andorra": "Andorra",
|
||||||
|
"angola": "Angola",
|
||||||
|
"anguilla": "Anguilla",
|
||||||
|
"antarctica": "Antarctica",
|
||||||
|
"antigua_and_barbuda": "Antigua and barbuda",
|
||||||
|
"argentina": "Argentina",
|
||||||
|
"armenia": "Armenia",
|
||||||
|
"aruba": "Aruba",
|
||||||
|
"australia": "Australia",
|
||||||
|
"austria": "Austria",
|
||||||
|
"azerbaijan": "Azerbaijan",
|
||||||
|
"bahamas": "Bahamas",
|
||||||
|
"bahrain": "Bahrain",
|
||||||
|
"bangladesh": "Bangladesh",
|
||||||
|
"barbados": "Barbados",
|
||||||
|
"belarus": "Belarus",
|
||||||
|
"belgium": "Belgium",
|
||||||
|
"belize": "Belize",
|
||||||
|
"benin": "Benin",
|
||||||
|
"bermuda": "Bermuda",
|
||||||
|
"bhutan": "Bhutan",
|
||||||
|
"bolivia": "Bolivia",
|
||||||
|
"bosnia_and_herzegovina": "Bosnia and herzegovina",
|
||||||
|
"botswana": "Botswana",
|
||||||
|
"brazil": "Brazil",
|
||||||
|
"british_indian_ocean_territory": "British indian ocean territory",
|
||||||
|
"british_virgin_islands": "British virgin islands",
|
||||||
|
"brunei": "Brunei",
|
||||||
|
"bulgaria": "Bulgaria",
|
||||||
|
"burkina_faso": "Burkina faso",
|
||||||
|
"burundi": "Burundi",
|
||||||
|
"cambodia": "Cambodia",
|
||||||
|
"cameroon": "Cameroon",
|
||||||
|
"canada": "Canada",
|
||||||
|
"cape_verde": "Cape verde",
|
||||||
|
"cayman_islands": "Cayman islands",
|
||||||
|
"central_african_republic": "Central african republic",
|
||||||
|
"chad": "Chad",
|
||||||
|
"chile": "Chile",
|
||||||
|
"china": "China",
|
||||||
|
"christmas_island": "Christmas island",
|
||||||
|
"cocos_keeling_islands": "Cocos keeling islands",
|
||||||
|
"colombia": "Colombia",
|
||||||
|
"comoros": "Comoros",
|
||||||
|
"congo_brazzaville": "Congo brazzaville",
|
||||||
|
"congo_kinshasa": "Congo kinshasa",
|
||||||
|
"cook_islands": "Cook islands",
|
||||||
|
"costa_rica": "Costa rica",
|
||||||
|
"cote_divoire": "Cote divoire",
|
||||||
|
"croatia": "Croatia",
|
||||||
|
"cuba": "Cuba",
|
||||||
|
"curacao": "Curacao",
|
||||||
|
"cyprus": "Cyprus",
|
||||||
|
"czech_republic": "Czech republic",
|
||||||
|
"denmark": "Denmark",
|
||||||
|
"djibouti": "Djibouti",
|
||||||
|
"dominica": "Dominica",
|
||||||
|
"dominican_republic": "Dominican republic",
|
||||||
|
"ecuador": "Ecuador",
|
||||||
|
"egypt": "Egypt",
|
||||||
|
"el_salvador": "El salvador",
|
||||||
|
"equatorial_guinea": "Equatorial guinea",
|
||||||
|
"eritrea": "Eritrea",
|
||||||
|
"estonia": "Estonia",
|
||||||
|
"eswatini": "Eswatini",
|
||||||
|
"ethiopia": "Ethiopia",
|
||||||
|
"falkland_islands": "Falkland islands",
|
||||||
|
"faroe_islands": "Faroe islands",
|
||||||
|
"fiji": "Fiji",
|
||||||
|
"finland": "Finland",
|
||||||
|
"france": "France",
|
||||||
|
"french_guiana": "French guiana",
|
||||||
|
"french_polynesia": "French polynesia",
|
||||||
|
"gabon": "Gabon",
|
||||||
|
"gambia": "Gambia",
|
||||||
|
"georgia": "Georgia",
|
||||||
|
"germany": "Germany",
|
||||||
|
"ghana": "Ghana",
|
||||||
|
"gibraltar": "Gibraltar",
|
||||||
|
"greece": "Greece",
|
||||||
|
"greenland": "Greenland",
|
||||||
|
"grenada": "Grenada",
|
||||||
|
"guadeloupe": "Guadeloupe",
|
||||||
|
"guam": "Guam",
|
||||||
|
"guatemala": "Guatemala",
|
||||||
|
"guernsey": "Guernsey",
|
||||||
|
"guinea": "Guinea",
|
||||||
|
"guinea_bissau": "Guinea bissau",
|
||||||
|
"guyana": "Guyana",
|
||||||
|
"haiti": "Haiti",
|
||||||
|
"honduras": "Honduras",
|
||||||
|
"hong_kong": "Hong kong",
|
||||||
|
"hungary": "Hungary",
|
||||||
|
"iceland": "Iceland",
|
||||||
|
"india": "India",
|
||||||
|
"indonesia": "Indonesia",
|
||||||
|
"iran": "Iran",
|
||||||
|
"iraq": "Iraq",
|
||||||
|
"ireland": "Ireland",
|
||||||
|
"isle_of_man": "Isle of man",
|
||||||
|
"israel": "Israel",
|
||||||
|
"italy": "Italy",
|
||||||
|
"jamaica": "Jamaica",
|
||||||
|
"japan": "Japan",
|
||||||
|
"jersey": "Jersey",
|
||||||
|
"jordan": "Jordan",
|
||||||
|
"kazakhstan": "Kazakhstan",
|
||||||
|
"kenya": "Kenya",
|
||||||
|
"kiribati": "Kiribati",
|
||||||
|
"kosovo": "Kosovo",
|
||||||
|
"kuwait": "Kuwait",
|
||||||
|
"kyrgyzstan": "Kyrgyzstan",
|
||||||
|
"laos": "Laos",
|
||||||
|
"latvia": "Latvia",
|
||||||
|
"lebanon": "Lebanon",
|
||||||
|
"lesotho": "Lesotho",
|
||||||
|
"liberia": "Liberia",
|
||||||
|
"libya": "Libya",
|
||||||
|
"liechtenstein": "Liechtenstein",
|
||||||
|
"lithuania": "Lithuania",
|
||||||
|
"luxembourg": "Luxembourg",
|
||||||
|
"macau": "Macau",
|
||||||
|
"madagascar": "Madagascar",
|
||||||
|
"malawi": "Malawi",
|
||||||
|
"malaysia": "Malaysia",
|
||||||
|
"maldives": "Maldives",
|
||||||
|
"mali": "Mali",
|
||||||
|
"malta": "Malta",
|
||||||
|
"marshall_islands": "Marshall islands",
|
||||||
|
"martinique": "Martinique",
|
||||||
|
"mauritania": "Mauritania",
|
||||||
|
"mauritius": "Mauritius",
|
||||||
|
"mayotte": "Mayotte",
|
||||||
|
"mexico": "Mexico",
|
||||||
|
"micronesia": "Micronesia",
|
||||||
|
"moldova": "Moldova",
|
||||||
|
"monaco": "Monaco",
|
||||||
|
"mongolia": "Mongolia",
|
||||||
|
"montenegro": "Montenegro",
|
||||||
|
"montserrat": "Montserrat",
|
||||||
|
"morocco": "Morocco",
|
||||||
|
"mozambique": "Mozambique",
|
||||||
|
"myanmar": "Myanmar",
|
||||||
|
"namibia": "Namibia",
|
||||||
|
"nauru": "Nauru",
|
||||||
|
"nepal": "Nepal",
|
||||||
|
"netherlands": "Netherlands",
|
||||||
|
"new_caledonia": "New caledonia",
|
||||||
|
"new_zealand": "New zealand",
|
||||||
|
"nicaragua": "Nicaragua",
|
||||||
|
"niger": "Niger",
|
||||||
|
"nigeria": "Nigeria",
|
||||||
|
"niue": "Niue",
|
||||||
|
"norfolk_island": "Norfolk island",
|
||||||
|
"north_korea": "North korea",
|
||||||
|
"north_macedonia": "North macedonia",
|
||||||
|
"northern_mariana_islands": "Northern mariana islands",
|
||||||
|
"norway": "Norway",
|
||||||
|
"oman": "Oman",
|
||||||
|
"pakistan": "Pakistan",
|
||||||
|
"palau": "Palau",
|
||||||
|
"palestine": "Palestine",
|
||||||
|
"panama": "Panama",
|
||||||
|
"papua_new_guinea": "Papua new guinea",
|
||||||
|
"paraguay": "Paraguay",
|
||||||
|
"peru": "Peru",
|
||||||
|
"philippines": "Philippines",
|
||||||
|
"pitcairn_islands": "Pitcairn islands",
|
||||||
|
"poland": "Poland",
|
||||||
|
"portugal": "Portugal",
|
||||||
|
"puerto_rico": "Puerto rico",
|
||||||
|
"qatar": "Qatar",
|
||||||
|
"reunion": "Reunion",
|
||||||
|
"romania": "Romania",
|
||||||
|
"russia": "Russia",
|
||||||
|
"rwanda": "Rwanda",
|
||||||
|
"saint_barthelemy": "Saint barthelemy",
|
||||||
|
"saint_helena": "Saint helena",
|
||||||
|
"saint_kitts_and_nevis": "Saint kitts and nevis",
|
||||||
|
"saint_lucia": "Saint lucia",
|
||||||
|
"saint_martin": "Saint martin",
|
||||||
|
"saint_pierre_and_miquelon": "Saint pierre and miquelon",
|
||||||
|
"saint_vincent_and_the_grenadines": "Saint vincent and the grenadines",
|
||||||
|
"samoa": "Samoa",
|
||||||
|
"san_marino": "San marino",
|
||||||
|
"sao_tome_and_principe": "Sao tome and principe",
|
||||||
|
"saudi_arabia": "Saudi arabia",
|
||||||
|
"senegal": "Senegal",
|
||||||
|
"serbia": "Serbia",
|
||||||
|
"seychelles": "Seychelles",
|
||||||
|
"sierra_leone": "Sierra leone",
|
||||||
|
"singapore": "Singapore",
|
||||||
|
"sint_maarten": "Sint maarten",
|
||||||
|
"slovakia": "Slovakia",
|
||||||
|
"slovenia": "Slovenia",
|
||||||
|
"solomon_islands": "Solomon islands",
|
||||||
|
"somalia": "Somalia",
|
||||||
|
"south_africa": "South africa",
|
||||||
|
"south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands",
|
||||||
|
"south_korea": "South korea",
|
||||||
|
"south_sudan": "South sudan",
|
||||||
|
"spain": "Spain",
|
||||||
|
"sri_lanka": "Sri lanka",
|
||||||
|
"sudan": "Sudan",
|
||||||
|
"suriname": "Suriname",
|
||||||
|
"svalbard_and_jan_mayen": "Svalbard and jan mayen",
|
||||||
|
"sweden": "Sweden",
|
||||||
|
"switzerland": "Switzerland",
|
||||||
|
"syria": "Syria",
|
||||||
|
"taiwan": "Taiwan",
|
||||||
|
"tajikistan": "Tajikistan",
|
||||||
|
"tanzania": "Tanzania",
|
||||||
|
"thailand": "Thailand",
|
||||||
|
"timor_leste": "Timor leste",
|
||||||
|
"togo": "Togo",
|
||||||
|
"tokelau": "Tokelau",
|
||||||
|
"tonga": "Tonga",
|
||||||
|
"trinidad_and_tobago": "Trinidad and tobago",
|
||||||
|
"tunisia": "Tunisia",
|
||||||
|
"turkey": "Turkey",
|
||||||
|
"turkmenistan": "Turkmenistan",
|
||||||
|
"turks_and_caicos_islands": "Turks and caicos islands",
|
||||||
|
"tuvalu": "Tuvalu",
|
||||||
|
"us_virgin_islands": "Us virgin islands",
|
||||||
|
"uganda": "Uganda",
|
||||||
|
"ukraine": "Ukraine",
|
||||||
|
"united_arab_emirates": "United arab emirates",
|
||||||
|
"united_kingdom": "United kingdom",
|
||||||
|
"united_states": "United states",
|
||||||
|
"uruguay": "Uruguay",
|
||||||
|
"uzbekistan": "Uzbekistan",
|
||||||
|
"vanuatu": "Vanuatu",
|
||||||
|
"vatican_city": "Vatican city",
|
||||||
|
"venezuela": "Venezuela",
|
||||||
|
"vietnam": "Vietnam",
|
||||||
|
"wallis_and_futuna": "Wallis and futuna",
|
||||||
|
"western_sahara": "Western sahara",
|
||||||
|
"yemen": "Yemen",
|
||||||
|
"zambia": "Zambia",
|
||||||
|
"zimbabwe": "Zimbabwe"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
public/locales/fa/authentication.json
Normal file
55
public/locales/fa/authentication.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"loginForm": {
|
||||||
|
"title": "ورود/ثبتنام",
|
||||||
|
"description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.",
|
||||||
|
"emailOrPhoneLabel": "شماره موبایل/ایمیل",
|
||||||
|
"submitButton": "ورود/ثبتنام",
|
||||||
|
"loginWithGoogle": "ورود با گوگل",
|
||||||
|
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
|
||||||
|
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
|
||||||
|
"thisFieldIsRequired": "این فیلد الزامی است"
|
||||||
|
},
|
||||||
|
"verify": {
|
||||||
|
"verify": "اعتبارسنجی",
|
||||||
|
"a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.",
|
||||||
|
"confirmAndLogin": "تایید و ورود",
|
||||||
|
"confirmAndContinue": "تایید و ادامه",
|
||||||
|
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید.",
|
||||||
|
"a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "کد تایید ۴ رقمی به شماره ایمیل شما ارسال شد. لطفا آن را وارد کنید.",
|
||||||
|
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
|
||||||
|
"theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
|
||||||
|
"youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
|
||||||
|
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد",
|
||||||
|
"resendCodeIn": "ارسال مجدد کد تا",
|
||||||
|
"moreMinute": "دقیقه دیگر",
|
||||||
|
"resendCode": "ارسال مجدد"
|
||||||
|
},
|
||||||
|
"completeSignUp": {
|
||||||
|
"completeSignUp": "تکمیل ثبت نام",
|
||||||
|
"emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "ایمیل {{ email }} با موفقیت تایید شد. برای ادامه لطفا شماره تماس خود را وارد کنید",
|
||||||
|
"phoneNumber": "شماره تماس"
|
||||||
|
},
|
||||||
|
"enterPassword": {
|
||||||
|
"loginWithPassword": "ورود با رمز",
|
||||||
|
"enterThePasswordYouSetForYourAccount": "رمز ورودی که برای اکانت خود تعیین کردید را وارد کنید",
|
||||||
|
"loginPassword": "رمز ورود",
|
||||||
|
"loginWithOneTimeCode": "ورود با کد یکبار مصرف",
|
||||||
|
"iForgotMyPassword": "رمز ورودم را فراموش کردم"
|
||||||
|
},
|
||||||
|
"forgetPassword": {
|
||||||
|
"forgetPassword": "فراموشی رمز",
|
||||||
|
"pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "لطفا برای بازیابی رمز عبور شماره موبایل/ایمیل خود را وارد کنید.",
|
||||||
|
"anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "یک ایمیل حاوی کد بازیابی به این ایمیل ارسال شد",
|
||||||
|
"anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "یک کد بازیابی به این شماره ارسال شد",
|
||||||
|
"confirm": "تایید",
|
||||||
|
"changePassword": "تغییر رمز عبور",
|
||||||
|
"createANewPassword": "یک رمز عبور جدید ایجاد کنید",
|
||||||
|
"newPassword": "رمز عبور جدید",
|
||||||
|
"includingANumber": "شامل عدد",
|
||||||
|
"atLeast8Characters": "حداقل ۸ حرف",
|
||||||
|
"containsAnUppercaseAndLowercaseLetter": "شامل یک حرف بزرگ و کوچک",
|
||||||
|
"ContainsASymbol": "شامل علامت (!@#$%&*^)",
|
||||||
|
"confirmPassword": "تکرار رمز عبور",
|
||||||
|
"passwordChangedSuccessfully": "رمز عبور با موفقیت تغییر یافت"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,202 @@
|
|||||||
{
|
{
|
||||||
"helloWorld": "سلام دنیا"
|
"labels": {
|
||||||
|
"search": "جست و جو"
|
||||||
|
},
|
||||||
|
"country": {
|
||||||
|
"afghanistan": "افغانستان",
|
||||||
|
"aland_islands": "جزایر آلند",
|
||||||
|
"albania": "آلبانی",
|
||||||
|
"algeria": "الجزایر",
|
||||||
|
"american_samoa": "ساموای آمریکایی",
|
||||||
|
"andorra": "آندورا",
|
||||||
|
"angola": "آنگولا",
|
||||||
|
"anguilla": "آنگویلا",
|
||||||
|
"antarctica": "جنوبگان",
|
||||||
|
"antigua_and_barbuda": "آنتیگوا و باربودا",
|
||||||
|
"argentina": "آرژانتین",
|
||||||
|
"armenia": "ارمنستان",
|
||||||
|
"aruba": "آروبا",
|
||||||
|
"australia": "استرالیا",
|
||||||
|
"austria": "اتریش",
|
||||||
|
"azerbaijan": "آذربایجان",
|
||||||
|
"bahamas": "باهاما",
|
||||||
|
"bahrain": "بحرین",
|
||||||
|
"bangladesh": "بنگلادش",
|
||||||
|
"barbados": "باربادوس",
|
||||||
|
"belarus": "بلاروس",
|
||||||
|
"belgium": "بلژیک",
|
||||||
|
"belize": "بلیز",
|
||||||
|
"benin": "بنین",
|
||||||
|
"bermuda": "برمودا",
|
||||||
|
"bhutan": "بوتان",
|
||||||
|
"bolivia": "بولیوی",
|
||||||
|
"bosnia_and_herzegovina": "بوسنی و هرزگوین",
|
||||||
|
"botswana": "بوتسوانا",
|
||||||
|
"brazil": "برزیل",
|
||||||
|
"british_virgin_islands": "جزایر ویرجین بریتانیا",
|
||||||
|
"brunei": "برونئی",
|
||||||
|
"bulgaria": "بلغارستان",
|
||||||
|
"burkina_faso": "بورکینافاسو",
|
||||||
|
"burundi": "بوروندی",
|
||||||
|
"cambodia": "کامبوج",
|
||||||
|
"cameroon": "کامرون",
|
||||||
|
"canada": "کانادا",
|
||||||
|
"cape_verde": "کیپ ورد",
|
||||||
|
"cayman_islands": "جزایر کیمن",
|
||||||
|
"central_african_republic": "جمهوری آفریقای مرکزی",
|
||||||
|
"chad": "چاد",
|
||||||
|
"chile": "شیلی",
|
||||||
|
"china": "چین",
|
||||||
|
"colombia": "کلمبیا",
|
||||||
|
"comoros": "کومور",
|
||||||
|
"costa_rica": "کاستاریکا",
|
||||||
|
"cote_divoire": "ساحل عاج",
|
||||||
|
"croatia": "کرواسی",
|
||||||
|
"cuba": "کوبا",
|
||||||
|
"cyprus": "قبرس",
|
||||||
|
"czech_republic": "جمهوری چک",
|
||||||
|
"denmark": "دانمارک",
|
||||||
|
"djibouti": "جیبوتی",
|
||||||
|
"dominica": "دومینیکا",
|
||||||
|
"dominican_republic": "جمهوری دومینیکن",
|
||||||
|
"ecuador": "اکوادور",
|
||||||
|
"egypt": "مصر",
|
||||||
|
"el_salvador": "السالوادور",
|
||||||
|
"equatorial_guinea": "گینه استوایی",
|
||||||
|
"eritrea": "اریتره",
|
||||||
|
"estonia": "استونی",
|
||||||
|
"eswatini": "سوازیلند",
|
||||||
|
"ethiopia": "اتیوپی",
|
||||||
|
"fiji": "فیجی",
|
||||||
|
"finland": "فنلاند",
|
||||||
|
"france": "فرانسه",
|
||||||
|
"gabon": "گابن",
|
||||||
|
"gambia": "گامبیا",
|
||||||
|
"georgia": "گرجستان",
|
||||||
|
"germany": "آلمان",
|
||||||
|
"ghana": "غنا",
|
||||||
|
"greece": "یونان",
|
||||||
|
"guatemala": "گواتمالا",
|
||||||
|
"guinea": "گینه",
|
||||||
|
"guinea_bissau": "گینه بیسائو",
|
||||||
|
"guyana": "گویان",
|
||||||
|
"haiti": "هائیتی",
|
||||||
|
"honduras": "هندوراس",
|
||||||
|
"hungary": "مجارستان",
|
||||||
|
"iceland": "ایسلند",
|
||||||
|
"india": "هندوستان",
|
||||||
|
"indonesia": "اندونزی",
|
||||||
|
"iran": "ایران",
|
||||||
|
"iraq": "عراق",
|
||||||
|
"ireland": "ایرلند",
|
||||||
|
"israel": "اسرائیل",
|
||||||
|
"italy": "ایتالیا",
|
||||||
|
"jamaica": "جامائیکا",
|
||||||
|
"japan": "ژاپن",
|
||||||
|
"jordan": "اردن",
|
||||||
|
"kazakhstan": "قزاقستان",
|
||||||
|
"kenya": "کنیا",
|
||||||
|
"kuwait": "کویت",
|
||||||
|
"kyrgyzstan": "قرقیزستان",
|
||||||
|
"laos": "لائوس",
|
||||||
|
"latvia": "لتونی",
|
||||||
|
"lebanon": "لبنان",
|
||||||
|
"lesotho": "لسوتو",
|
||||||
|
"liberia": "لیبریا",
|
||||||
|
"libya": "لیبی",
|
||||||
|
"luxembourg": "لوکزامبورگ",
|
||||||
|
"malaysia": "مالزی",
|
||||||
|
"maldives": "مالدیو",
|
||||||
|
"mali": "مالی",
|
||||||
|
"malta": "مالت",
|
||||||
|
"mauritania": "موریتانی",
|
||||||
|
"mauritius": "موریس",
|
||||||
|
"mexico": "مکزیک",
|
||||||
|
"moldova": "مولداوی",
|
||||||
|
"monaco": "موناکو",
|
||||||
|
"mongolia": "مغولستان",
|
||||||
|
"morocco": "مراکش",
|
||||||
|
"mozambique": "موزامبیک",
|
||||||
|
"myanmar": "میانمار",
|
||||||
|
"namibia": "نامیبیا",
|
||||||
|
"nepal": "نپال",
|
||||||
|
"netherlands": "هلند",
|
||||||
|
"new_zealand": "نیوزیلند",
|
||||||
|
"nicaragua": "نیکاراگوئه",
|
||||||
|
"niger": "نیجر",
|
||||||
|
"nigeria": "نیجریه",
|
||||||
|
"north_korea": "کره شمالی",
|
||||||
|
"north_macedonia": "مقدونیه",
|
||||||
|
"norway": "نروژ",
|
||||||
|
"oman": "عمان",
|
||||||
|
"pakistan": "پاکستان",
|
||||||
|
"palau": "پالائو",
|
||||||
|
"panama": "پاناما",
|
||||||
|
"papua_new_guinea": "پاپوآ گینه نو",
|
||||||
|
"paraguay": "پاراگوئه",
|
||||||
|
"peru": "پرو",
|
||||||
|
"philippines": "فیلیپین",
|
||||||
|
"poland": "لهستان",
|
||||||
|
"portugal": "پرتغال",
|
||||||
|
"qatar": "قطر",
|
||||||
|
"romania": "رومانی",
|
||||||
|
"russia": "روسیه",
|
||||||
|
"rwanda": "رواندا",
|
||||||
|
"saudi_arabia": "عربستان سعودی",
|
||||||
|
"senegal": "سنگال",
|
||||||
|
"serbia": "صربستان",
|
||||||
|
"seychelles": "سیشل",
|
||||||
|
"sierra_leone": "سیرالئون",
|
||||||
|
"singapore": "سنگاپور",
|
||||||
|
"south_africa": "آفریقای جنوبی",
|
||||||
|
"south_korea": "کره جنوبی",
|
||||||
|
"south_sudan": "سودان جنوبی",
|
||||||
|
"spain": "اسپانیا",
|
||||||
|
"sri_lanka": "سریلانکا",
|
||||||
|
"sudan": "سودان",
|
||||||
|
"suriname": "سورینام",
|
||||||
|
"sweden": "سوئد",
|
||||||
|
"switzerland": "سوئیس",
|
||||||
|
"syria": "سوریه",
|
||||||
|
"taiwan": "تایوان",
|
||||||
|
"tajikistan": "تاجیکستان",
|
||||||
|
"tanzania": "تانزانیا",
|
||||||
|
"thailand": "تایلند",
|
||||||
|
"timor_leste": "تیمور شرقی",
|
||||||
|
"togo": "توگو",
|
||||||
|
"tonga": "تونگا",
|
||||||
|
"trinidad_and_tobago": "ترینیداد و توباگو",
|
||||||
|
"tunisia": "تونس",
|
||||||
|
"turkey": "ترکیه",
|
||||||
|
"turkmenistan": "ترکمنستان",
|
||||||
|
"tuvalu": "تووالو",
|
||||||
|
"uganda": "اوگاندا",
|
||||||
|
"ukraine": "اوکراین",
|
||||||
|
"united_arab_emirates": "امارات متحده عربی",
|
||||||
|
"united_kingdom": "انگلستان",
|
||||||
|
"united_states": "ایالات متحده آمریکا",
|
||||||
|
"uruguay": "اروگوئه",
|
||||||
|
"uzbekistan": "ازبکستان",
|
||||||
|
"vanuatu": "وانواتو",
|
||||||
|
"venezuela": "ونزوئلا",
|
||||||
|
"vietnam": "ویتنام",
|
||||||
|
"yemen": "یمن",
|
||||||
|
"zambia": "زامبیا",
|
||||||
|
"zimbabwe": "زیمبابوه"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"noResualtFound": "نتیجه ای یافت نشد."
|
||||||
|
},
|
||||||
|
"side": {
|
||||||
|
"account": "حساب کاربری",
|
||||||
|
"personalInfo": "اطلاعات شخصی",
|
||||||
|
"contactInfo": "شماره تماس",
|
||||||
|
"email": "ایمیل",
|
||||||
|
"security": "امنیت",
|
||||||
|
"password": "رمز عبور",
|
||||||
|
"confirmedIps": "آدرس های تایید شده",
|
||||||
|
"recentSessions": "ورود های اخیر",
|
||||||
|
"activeSessions": "نشست های فعال",
|
||||||
|
"setting": "تنظیمات"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/App.tsx
25
src/App.tsx
@@ -1,32 +1,17 @@
|
|||||||
import { CssBaseline, useColorScheme } from '@mui/material';
|
import { CssBaseline } from '@mui/material';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { LanguageManager } from './components/LanguageManager';
|
import { LanguageManager } from '@/components/LanguageManager';
|
||||||
import { UserCompletionForm } from './features/authentication/components/UserCompletionForm';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
import { router } from '@/routes';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<LanguageManager />
|
<LanguageManager />
|
||||||
<UserCompletionForm />
|
<RouterProvider router={router} />
|
||||||
<ThemeToggleButton />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
import { Button } from '@mui/material';
|
|
||||||
|
|
||||||
export const ThemeToggleButton = () => {
|
|
||||||
const { mode, setMode } = useColorScheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}
|
|
||||||
>
|
|
||||||
Switch to {mode === 'light' ? 'Dark' : 'Light'} Mode
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
35
src/components/Layout/Header.tsx
Normal file
35
src/components/Layout/Header.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Box, IconButton, Typography } from '@mui/material';
|
||||||
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { More } from 'iconsax-react';
|
||||||
|
import type { User } from './type';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = ({ user }) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
height: (t) => t.spacing(10.5),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{user.firstName + ' ' + user.lastName}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
{user.phoneNumber}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<IconButton>
|
||||||
|
<Icon Component={More} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
src/components/Layout/Layout.tsx
Normal file
69
src/components/Layout/Layout.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { SideNav } from '@rkheftan/harmony-ui';
|
||||||
|
import { buildNavItems } from './buildNavItems';
|
||||||
|
import { appRoutes } from '@/routes/config';
|
||||||
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||||
|
import { Header } from './Header';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Toolbar } from './Toolbar';
|
||||||
|
import type { User } from './type';
|
||||||
|
|
||||||
|
export const Layout = () => {
|
||||||
|
const navItemConfigs = buildNavItems(appRoutes);
|
||||||
|
const location = useLocation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [sideNavOpen, setSideNavOpen] = useState(false);
|
||||||
|
const [user] = useState<User>({
|
||||||
|
firstName: 'محمد حسین',
|
||||||
|
lastName: 'برزه گر',
|
||||||
|
phoneNumber: '09123456789',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
// TODO: Check for better approach
|
||||||
|
width: isMobile ? '100%' : 1070,
|
||||||
|
height: isMobile ? '100vh' : '80%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
borderRadius: isMobile ? 0 : 4,
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Toolbar
|
||||||
|
isMobile={isMobile}
|
||||||
|
sideNavOpen={sideNavOpen}
|
||||||
|
setSideNavOpen={setSideNavOpen}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
<Outlet />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<SideNav
|
||||||
|
open={sideNavOpen}
|
||||||
|
onClose={() => setSideNavOpen(false)}
|
||||||
|
header={isMobile ? undefined : <Header user={user} />}
|
||||||
|
footer={isMobile ? <Header user={user} /> : undefined}
|
||||||
|
navConfig={navItemConfigs}
|
||||||
|
activePath={location.pathname + location.hash}
|
||||||
|
selectedVariant="textOnly"
|
||||||
|
positioning="absolute"
|
||||||
|
sideNavVariant={isMobile ? 'temporary' : 'full'}
|
||||||
|
top={8.125}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
75
src/components/Layout/Toolbar.tsx
Normal file
75
src/components/Layout/Toolbar.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
Toolbar as MuiToolbar,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
|
import { HambergerMenu, Menu } from 'iconsax-react';
|
||||||
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
import type { User } from './type';
|
||||||
|
|
||||||
|
interface ToolbarProps {
|
||||||
|
sideNavOpen: boolean;
|
||||||
|
setSideNavOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
isMobile: boolean;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Toolbar: React.FC<ToolbarProps> = ({
|
||||||
|
sideNavOpen,
|
||||||
|
setSideNavOpen,
|
||||||
|
isMobile,
|
||||||
|
user,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<MuiToolbar
|
||||||
|
sx={{
|
||||||
|
height: (t) => t.spacing(isMobile ? 8 : 10.5),
|
||||||
|
px: isMobile ? 3 : 2,
|
||||||
|
borderBottom: (t) => `1px solid ${t.palette.divider}`,
|
||||||
|
boxSizing: 'content-box',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', height: '100%', alignItems: 'center', gap: 2 }}
|
||||||
|
>
|
||||||
|
{isMobile && (
|
||||||
|
<IconButton
|
||||||
|
sx={{
|
||||||
|
backgroundColor: sideNavOpen ? 'primary.light' : undefined,
|
||||||
|
color: sideNavOpen ? 'primary.main' : undefined,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: sideNavOpen ? 'primary.light' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => setSideNavOpen(!sideNavOpen)}
|
||||||
|
>
|
||||||
|
<Icon Component={HambergerMenu} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{/* <Logo /> */}
|
||||||
|
<Typography variant="h6">LOGO placeholder</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{ display: 'flex', height: '100%', alignItems: 'center', gap: 1 }}
|
||||||
|
>
|
||||||
|
{isMobile && (
|
||||||
|
<Avatar
|
||||||
|
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
||||||
|
src={user.profileUrl}
|
||||||
|
>
|
||||||
|
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
<IconButton>
|
||||||
|
<Icon Component={Menu} variant="Bold" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</MuiToolbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/components/Layout/buildNavItems.tsx
Normal file
34
src/components/Layout/buildNavItems.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/components/SideNav.tsx (Conceptual Example)
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { type RouteConfig } from '@/routes/config';
|
||||||
|
import { Icon, type NavItemConfig } from '@rkheftan/harmony-ui';
|
||||||
|
import type { Icon as Iconsax } from 'iconsax-react';
|
||||||
|
|
||||||
|
const getIcon = (icon?: Iconsax) => (isSelected: boolean) =>
|
||||||
|
icon ? (
|
||||||
|
<Icon Component={icon} variant={isSelected ? 'Bold' : 'Broken'} />
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
export function buildNavItems(routes: RouteConfig[]): NavItemConfig[] {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return routes.flatMap((route) => {
|
||||||
|
// Check if route itself does not have a navItem but its child has
|
||||||
|
if (!route.navConfig && route.children) {
|
||||||
|
return buildNavItems(route.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if route.navConfig is defined before destructuring
|
||||||
|
if (!route.navConfig) {
|
||||||
|
return []; // Return an empty array to be flattened
|
||||||
|
}
|
||||||
|
const { title, icon } = route.navConfig;
|
||||||
|
return {
|
||||||
|
text: t(title),
|
||||||
|
getIcon: getIcon(icon),
|
||||||
|
path: route.path,
|
||||||
|
children: route.children ? buildNavItems(route.children) : undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
8
src/components/Layout/type.ts
Normal file
8
src/components/Layout/type.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// TODO: this type file is temporary and should replace it with the actual use type and value when api is ready
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
profileUrl?: string;
|
||||||
|
}
|
||||||
@@ -9,7 +9,11 @@ export interface ToastProps extends PropsWithChildren {
|
|||||||
|
|
||||||
export const Toast = ({ color, open, onClose, children }: ToastProps) => {
|
export const Toast = ({ color, open, onClose, children }: ToastProps) => {
|
||||||
return (
|
return (
|
||||||
<Snackbar sx={{ minWidth: '396px' }} open={open} onClose={onClose}>
|
<Snackbar
|
||||||
|
sx={{ width: (t) => `calc(100% - ${t.spacing(6)})`, maxWidth: '396px' }}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
<Alert
|
<Alert
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
severity={color}
|
severity={color}
|
||||||
|
|||||||
124
src/components/components/DigitsInput.tsx
Normal file
124
src/components/components/DigitsInput.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useEffect,
|
||||||
|
type SetStateAction,
|
||||||
|
type Dispatch,
|
||||||
|
useState,
|
||||||
|
type KeyboardEvent,
|
||||||
|
} from 'react';
|
||||||
|
import { TextField, Stack } from '@mui/material';
|
||||||
|
|
||||||
|
interface DigitInputProps {
|
||||||
|
error: boolean;
|
||||||
|
success: boolean;
|
||||||
|
onChange: Dispatch<SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DigitInput: React.FC<DigitInputProps> = ({
|
||||||
|
onChange,
|
||||||
|
error,
|
||||||
|
success,
|
||||||
|
}) => {
|
||||||
|
const [code, setCode] = useState<string[]>(['', '', '', '']);
|
||||||
|
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRefs.current[0]?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDigitInputValueChange = (value: string[]) => {
|
||||||
|
const formatted = value.filter((char) => char !== '').join('');
|
||||||
|
onChange(formatted);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (value: string, index: number) => {
|
||||||
|
if (!/^\d$/.test(value) && value !== '') return;
|
||||||
|
|
||||||
|
const newCode = [...code];
|
||||||
|
newCode[index] = value;
|
||||||
|
setCode(newCode);
|
||||||
|
handleDigitInputValueChange(newCode);
|
||||||
|
|
||||||
|
if (value && index < 4 - 1) {
|
||||||
|
inputRefs.current[index + 1]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackspace = (
|
||||||
|
event: KeyboardEvent<HTMLDivElement>,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (index >= 0) {
|
||||||
|
handleChange('', index);
|
||||||
|
inputRefs.current[index - 1]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaste = (event: React.ClipboardEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const pastedData = event.clipboardData.getData('text').replace(/\D/g, ''); // Remove non-digit characters
|
||||||
|
const newCode = [...code];
|
||||||
|
|
||||||
|
pastedData.split('').forEach((digit, i) => {
|
||||||
|
if (i < code.length) {
|
||||||
|
newCode[i] = digit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setCode(newCode);
|
||||||
|
handleDigitInputValueChange(newCode);
|
||||||
|
|
||||||
|
// Focus the next empty input after the last pasted character
|
||||||
|
const lastIndex = Math.min(pastedData.length, code.length) - 1;
|
||||||
|
if (lastIndex >= 0 && inputRefs.current[lastIndex]) {
|
||||||
|
inputRefs.current[lastIndex]?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row-reverse"
|
||||||
|
alignItems="center"
|
||||||
|
sx={{ gap: 2, width: '100%', my: 4 }}
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
{code.map((digit, index) => (
|
||||||
|
<TextField
|
||||||
|
error={error}
|
||||||
|
color={success ? 'success' : 'primary'}
|
||||||
|
key={index}
|
||||||
|
inputRef={(el) => (inputRefs.current[index] = el)}
|
||||||
|
value={digit}
|
||||||
|
onChange={(e) => handleChange(e.target.value, index)}
|
||||||
|
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
|
||||||
|
onPaste={(e) => handlePaste(e)}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: {
|
||||||
|
maxLength: 1,
|
||||||
|
sx: {
|
||||||
|
height: '72px',
|
||||||
|
color: error
|
||||||
|
? 'error.main'
|
||||||
|
: success
|
||||||
|
? 'success.main'
|
||||||
|
: 'text.primary',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '48px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant="standard"
|
||||||
|
size="medium"
|
||||||
|
sx={{
|
||||||
|
width: '83px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DigitInput;
|
||||||
8
src/components/components/common/Container.tsx
Normal file
8
src/components/components/common/Container.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
|
||||||
|
export const Container = styled(Box)(() => ({
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
margin: '0 auto',
|
||||||
|
}));
|
||||||
21
src/components/components/common/FlexBox.tsx
Normal file
21
src/components/components/common/FlexBox.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Box, styled, type BoxProps } from '@mui/material';
|
||||||
|
|
||||||
|
// Define the props our component will accept
|
||||||
|
interface FlexBoxProps extends BoxProps {
|
||||||
|
direction?: 'row' | 'column';
|
||||||
|
justify?: string;
|
||||||
|
align?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlexBox = styled(Box, {
|
||||||
|
// Do not forward these custom props to the DOM element
|
||||||
|
shouldForwardProp: (prop) =>
|
||||||
|
prop !== 'direction' && prop !== 'justify' && prop !== 'align',
|
||||||
|
})<FlexBoxProps>(
|
||||||
|
({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: direction,
|
||||||
|
justifyContent: justify,
|
||||||
|
alignItems: align,
|
||||||
|
}),
|
||||||
|
);
|
||||||
19
src/components/components/common/Stack.tsx
Normal file
19
src/components/components/common/Stack.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Box, styled, type BoxProps } from '@mui/material';
|
||||||
|
|
||||||
|
interface StackProps extends BoxProps {
|
||||||
|
direction?: 'row' | 'column';
|
||||||
|
spacing?: number; // Spacing factor (multiplied by theme.spacing)
|
||||||
|
align?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Stack = styled(Box, {
|
||||||
|
shouldForwardProp: (prop) =>
|
||||||
|
prop !== 'direction' && prop !== 'spacing' && prop !== 'align',
|
||||||
|
})<StackProps>(
|
||||||
|
({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: direction,
|
||||||
|
alignItems: align,
|
||||||
|
gap: theme.spacing(spacing),
|
||||||
|
}),
|
||||||
|
);
|
||||||
267
src/countries.ts
Normal file
267
src/countries.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
export interface Country {
|
||||||
|
code: string;
|
||||||
|
label: string;
|
||||||
|
phone: CountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const countries: readonly Country[] = [
|
||||||
|
{ code: 'AF', label: 'country.afghanistan', phone: '+93' },
|
||||||
|
{ code: 'AX', label: 'country.aland_islands', phone: '+358' },
|
||||||
|
{ code: 'AL', label: 'country.albania', phone: '+355' },
|
||||||
|
{ code: 'DZ', label: 'country.algeria', phone: '+213' },
|
||||||
|
{ code: 'AS', label: 'country.american_samoa', phone: '+1684' },
|
||||||
|
{ code: 'AD', label: 'country.andorra', phone: '+376' },
|
||||||
|
{ code: 'AO', label: 'country.angola', phone: '+244' },
|
||||||
|
{ code: 'AI', label: 'country.anguilla', phone: '+1264' },
|
||||||
|
{ code: 'AQ', label: 'country.antarctica', phone: '+672' },
|
||||||
|
{ code: 'AG', label: 'country.antigua_and_barbuda', phone: '+1268' },
|
||||||
|
{ code: 'AR', label: 'country.argentina', phone: '+54' },
|
||||||
|
{ code: 'AM', label: 'country.armenia', phone: '+374' },
|
||||||
|
{ code: 'AW', label: 'country.aruba', phone: '+297' },
|
||||||
|
{ code: 'AU', label: 'country.australia', phone: '+61' },
|
||||||
|
{ code: 'AT', label: 'country.austria', phone: '+43' },
|
||||||
|
{ code: 'AZ', label: 'country.azerbaijan', phone: '+994' },
|
||||||
|
{ code: 'BS', label: 'country.bahamas', phone: '+1242' },
|
||||||
|
{ code: 'BH', label: 'country.bahrain', phone: '+973' },
|
||||||
|
{ code: 'BD', label: 'country.bangladesh', phone: '+880' },
|
||||||
|
{ code: 'BB', label: 'country.barbados', phone: '+1246' },
|
||||||
|
{ code: 'BY', label: 'country.belarus', phone: '+375' },
|
||||||
|
{ code: 'BE', label: 'country.belgium', phone: '+32' },
|
||||||
|
{ code: 'BZ', label: 'country.belize', phone: '+501' },
|
||||||
|
{ code: 'BJ', label: 'country.benin', phone: '+229' },
|
||||||
|
{ code: 'BM', label: 'country.bermuda', phone: '+1441' },
|
||||||
|
{ code: 'BT', label: 'country.bhutan', phone: '+975' },
|
||||||
|
{ code: 'BO', label: 'country.bolivia', phone: '+591' },
|
||||||
|
{ code: 'BA', label: 'country.bosnia_and_herzegovina', phone: '+387' },
|
||||||
|
{ code: 'BW', label: 'country.botswana', phone: '+267' },
|
||||||
|
{ code: 'BR', label: 'country.brazil', phone: '+55' },
|
||||||
|
{
|
||||||
|
code: 'IO',
|
||||||
|
label: 'country.british_indian_ocean_territory',
|
||||||
|
phone: '+246',
|
||||||
|
},
|
||||||
|
{ code: 'VG', label: 'country.british_virgin_islands', phone: '+1284' },
|
||||||
|
{ code: 'BN', label: 'country.brunei', phone: '+673' },
|
||||||
|
{ code: 'BG', label: 'country.bulgaria', phone: '+359' },
|
||||||
|
{ code: 'BF', label: 'country.burkina_faso', phone: '+226' },
|
||||||
|
{ code: 'BI', label: 'country.burundi', phone: '+257' },
|
||||||
|
{ code: 'KH', label: 'country.cambodia', phone: '+855' },
|
||||||
|
{ code: 'CM', label: 'country.cameroon', phone: '+237' },
|
||||||
|
{ code: 'CA', label: 'country.canada', phone: '+1' },
|
||||||
|
{ code: 'CV', label: 'country.cape_verde', phone: '+238' },
|
||||||
|
{ code: 'KY', label: 'country.cayman_islands', phone: '+1345' },
|
||||||
|
{ code: 'CF', label: 'country.central_african_republic', phone: '+236' },
|
||||||
|
{ code: 'TD', label: 'country.chad', phone: '+235' },
|
||||||
|
{ code: 'CL', label: 'country.chile', phone: '+56' },
|
||||||
|
{ code: 'CN', label: 'country.china', phone: '+86' },
|
||||||
|
{ code: 'CX', label: 'country.christmas_island', phone: '+61' },
|
||||||
|
{ code: 'CC', label: 'country.cocos_keeling_islands', phone: '+61' },
|
||||||
|
{ code: 'CO', label: 'country.colombia', phone: '+57' },
|
||||||
|
{ code: 'KM', label: 'country.comoros', phone: '+269' },
|
||||||
|
{ code: 'CG', label: 'country.congo_brazzaville', phone: '+242' },
|
||||||
|
{ code: 'CD', label: 'country.congo_kinshasa', phone: '+243' },
|
||||||
|
{ code: 'CK', label: 'country.cook_islands', phone: '+682' },
|
||||||
|
{ code: 'CR', label: 'country.costa_rica', phone: '+506' },
|
||||||
|
{ code: 'CI', label: 'country.cote_divoire', phone: '+225' },
|
||||||
|
{ code: 'HR', label: 'country.croatia', phone: '+385' },
|
||||||
|
{ code: 'CU', label: 'country.cuba', phone: '+53' },
|
||||||
|
{ code: 'CW', label: 'country.curacao', phone: '+599' },
|
||||||
|
{ code: 'CY', label: 'country.cyprus', phone: '+357' },
|
||||||
|
{ code: 'CZ', label: 'country.czech_republic', phone: '+420' },
|
||||||
|
{ code: 'DK', label: 'country.denmark', phone: '+45' },
|
||||||
|
{ code: 'DJ', label: 'country.djibouti', phone: '+253' },
|
||||||
|
{ code: 'DM', label: 'country.dominica', phone: '+1767' },
|
||||||
|
{ code: 'DO', label: 'country.dominican_republic', phone: '+1' },
|
||||||
|
{ code: 'EC', label: 'country.ecuador', phone: '+593' },
|
||||||
|
{ code: 'EG', label: 'country.egypt', phone: '+20' },
|
||||||
|
{ code: 'SV', label: 'country.el_salvador', phone: '+503' },
|
||||||
|
{ code: 'GQ', label: 'country.equatorial_guinea', phone: '+240' },
|
||||||
|
{ code: 'ER', label: 'country.eritrea', phone: '+291' },
|
||||||
|
{ code: 'EE', label: 'country.estonia', phone: '+372' },
|
||||||
|
{ code: 'SZ', label: 'country.eswatini', phone: '+268' },
|
||||||
|
{ code: 'ET', label: 'country.ethiopia', phone: '+251' },
|
||||||
|
{ code: 'FK', label: 'country.falkland_islands', phone: '+500' },
|
||||||
|
{ code: 'FO', label: 'country.faroe_islands', phone: '+298' },
|
||||||
|
{ code: 'FJ', label: 'country.fiji', phone: '+679' },
|
||||||
|
{ code: 'FI', label: 'country.finland', phone: '+358' },
|
||||||
|
{ code: 'FR', label: 'country.france', phone: '+33' },
|
||||||
|
{ code: 'GF', label: 'country.french_guiana', phone: '+594' },
|
||||||
|
{ code: 'PF', label: 'country.french_polynesia', phone: '+689' },
|
||||||
|
{ code: 'GA', label: 'country.gabon', phone: '+241' },
|
||||||
|
{ code: 'GM', label: 'country.gambia', phone: '+220' },
|
||||||
|
{ code: 'GE', label: 'country.georgia', phone: '+995' },
|
||||||
|
{ code: 'DE', label: 'country.germany', phone: '+49' },
|
||||||
|
{ code: 'GH', label: 'country.ghana', phone: '+233' },
|
||||||
|
{ code: 'GI', label: 'country.gibraltar', phone: '+350' },
|
||||||
|
{ code: 'GR', label: 'country.greece', phone: '+30' },
|
||||||
|
{ code: 'GL', label: 'country.greenland', phone: '+299' },
|
||||||
|
{ code: 'GD', label: 'country.grenada', phone: '+1473' },
|
||||||
|
{ code: 'GP', label: 'country.guadeloupe', phone: '+590' },
|
||||||
|
{ code: 'GU', label: 'country.guam', phone: '+1671' },
|
||||||
|
{ code: 'GT', label: 'country.guatemala', phone: '+502' },
|
||||||
|
{ code: 'GG', label: 'country.guernsey', phone: '+44' },
|
||||||
|
{ code: 'GN', label: 'country.guinea', phone: '+224' },
|
||||||
|
{ code: 'GW', label: 'country.guinea_bissau', phone: '+245' },
|
||||||
|
{ code: 'GY', label: 'country.guyana', phone: '+592' },
|
||||||
|
{ code: 'HT', label: 'country.haiti', phone: '+509' },
|
||||||
|
{ code: 'HN', label: 'country.honduras', phone: '+504' },
|
||||||
|
{ code: 'HK', label: 'country.hong_kong', phone: '+852' },
|
||||||
|
{ code: 'HU', label: 'country.hungary', phone: '+36' },
|
||||||
|
{ code: 'IS', label: 'country.iceland', phone: '+354' },
|
||||||
|
{ code: 'IN', label: 'country.india', phone: '+91' },
|
||||||
|
{ code: 'ID', label: 'country.indonesia', phone: '+62' },
|
||||||
|
{ code: 'IR', label: 'country.iran', phone: '+98' },
|
||||||
|
{ code: 'IQ', label: 'country.iraq', phone: '+964' },
|
||||||
|
{ code: 'IE', label: 'country.ireland', phone: '+353' },
|
||||||
|
{ code: 'IM', label: 'country.isle_of_man', phone: '+44' },
|
||||||
|
{ code: 'IL', label: 'country.israel', phone: '+972' },
|
||||||
|
{ code: 'IT', label: 'country.italy', phone: '+39' },
|
||||||
|
{ code: 'JM', label: 'country.jamaica', phone: '+1876' },
|
||||||
|
{ code: 'JP', label: 'country.japan', phone: '+81' },
|
||||||
|
{ code: 'JE', label: 'country.jersey', phone: '+44' },
|
||||||
|
{ code: 'JO', label: 'country.jordan', phone: '+962' },
|
||||||
|
{ code: 'KZ', label: 'country.kazakhstan', phone: '+7' },
|
||||||
|
{ code: 'KE', label: 'country.kenya', phone: '+254' },
|
||||||
|
{ code: 'KI', label: 'country.kiribati', phone: '+686' },
|
||||||
|
{ code: 'XK', label: 'country.kosovo', phone: '+383' },
|
||||||
|
{ code: 'KW', label: 'country.kuwait', phone: '+965' },
|
||||||
|
{ code: 'KG', label: 'country.kyrgyzstan', phone: '+996' },
|
||||||
|
{ code: 'LA', label: 'country.laos', phone: '+856' },
|
||||||
|
{ code: 'LV', label: 'country.latvia', phone: '+371' },
|
||||||
|
{ code: 'LB', label: 'country.lebanon', phone: '+961' },
|
||||||
|
{ code: 'LS', label: 'country.lesotho', phone: '+266' },
|
||||||
|
{ code: 'LR', label: 'country.liberia', phone: '+231' },
|
||||||
|
{ code: 'LY', label: 'country.libya', phone: '+218' },
|
||||||
|
{ code: 'LI', label: 'country.liechtenstein', phone: '+423' },
|
||||||
|
{ code: 'LT', label: 'country.lithuania', phone: '+370' },
|
||||||
|
{ code: 'LU', label: 'country.luxembourg', phone: '+352' },
|
||||||
|
{ code: 'MO', label: 'country.macau', phone: '+853' },
|
||||||
|
{ code: 'MG', label: 'country.madagascar', phone: '+261' },
|
||||||
|
{ code: 'MW', label: 'country.malawi', phone: '+265' },
|
||||||
|
{ code: 'MY', label: 'country.malaysia', phone: '+60' },
|
||||||
|
{ code: 'MV', label: 'country.maldives', phone: '+960' },
|
||||||
|
{ code: 'ML', label: 'country.mali', phone: '+223' },
|
||||||
|
{ code: 'MT', label: 'country.malta', phone: '+356' },
|
||||||
|
{ code: 'MH', label: 'country.marshall_islands', phone: '+692' },
|
||||||
|
{ code: 'MQ', label: 'country.martinique', phone: '+596' },
|
||||||
|
{ code: 'MR', label: 'country.mauritania', phone: '+222' },
|
||||||
|
{ code: 'MU', label: 'country.mauritius', phone: '+230' },
|
||||||
|
{ code: 'YT', label: 'country.mayotte', phone: '+262' },
|
||||||
|
{ code: 'MX', label: 'country.mexico', phone: '+52' },
|
||||||
|
{ code: 'FM', label: 'country.micronesia', phone: '+691' },
|
||||||
|
{ code: 'MD', label: 'country.moldova', phone: '+373' },
|
||||||
|
{ code: 'MC', label: 'country.monaco', phone: '+377' },
|
||||||
|
{ code: 'MN', label: 'country.mongolia', phone: '+976' },
|
||||||
|
{ code: 'ME', label: 'country.montenegro', phone: '+382' },
|
||||||
|
{ code: 'MS', label: 'country.montserrat', phone: '+1664' },
|
||||||
|
{ code: 'MA', label: 'country.morocco', phone: '+212' },
|
||||||
|
{ code: 'MZ', label: 'country.mozambique', phone: '+258' },
|
||||||
|
{ code: 'MM', label: 'country.myanmar', phone: '+95' },
|
||||||
|
{ code: 'NA', label: 'country.namibia', phone: '+264' },
|
||||||
|
{ code: 'NR', label: 'country.nauru', phone: '+674' },
|
||||||
|
{ code: 'NP', label: 'country.nepal', phone: '+977' },
|
||||||
|
{ code: 'NL', label: 'country.netherlands', phone: '+31' },
|
||||||
|
{ code: 'NC', label: 'country.new_caledonia', phone: '+687' },
|
||||||
|
{ code: 'NZ', label: 'country.new_zealand', phone: '+64' },
|
||||||
|
{ code: 'NI', label: 'country.nicaragua', phone: '+505' },
|
||||||
|
{ code: 'NE', label: 'country.niger', phone: '+227' },
|
||||||
|
{ code: 'NG', label: 'country.nigeria', phone: '+234' },
|
||||||
|
{ code: 'NU', label: 'country.niue', phone: '+683' },
|
||||||
|
{ code: 'NF', label: 'country.norfolk_island', phone: '+672' },
|
||||||
|
{ code: 'KP', label: 'country.north_korea', phone: '+850' },
|
||||||
|
{ code: 'MK', label: 'country.north_macedonia', phone: '+389' },
|
||||||
|
{ code: 'MP', label: 'country.northern_mariana_islands', phone: '+1670' },
|
||||||
|
{ code: 'NO', label: 'country.norway', phone: '+47' },
|
||||||
|
{ code: 'OM', label: 'country.oman', phone: '+968' },
|
||||||
|
{ code: 'PK', label: 'country.pakistan', phone: '+92' },
|
||||||
|
{ code: 'PW', label: 'country.palau', phone: '+680' },
|
||||||
|
{ code: 'PS', label: 'country.palestine', phone: '+970' },
|
||||||
|
{ code: 'PA', label: 'country.panama', phone: '+507' },
|
||||||
|
{ code: 'PG', label: 'country.papua_new_guinea', phone: '+675' },
|
||||||
|
{ code: 'PY', label: 'country.paraguay', phone: '+595' },
|
||||||
|
{ code: 'PE', label: 'country.peru', phone: '+51' },
|
||||||
|
{ code: 'PH', label: 'country.philippines', phone: '+63' },
|
||||||
|
{ code: 'PN', label: 'country.pitcairn_islands', phone: '+64' },
|
||||||
|
{ code: 'PL', label: 'country.poland', phone: '+48' },
|
||||||
|
{ code: 'PT', label: 'country.portugal', phone: '+351' },
|
||||||
|
{ code: 'PR', label: 'country.puerto_rico', phone: '+1' },
|
||||||
|
{ code: 'QA', label: 'country.qatar', phone: '+974' },
|
||||||
|
{ code: 'RE', label: 'country.reunion', phone: '+262' },
|
||||||
|
{ code: 'RO', label: 'country.romania', phone: '+40' },
|
||||||
|
{ code: 'RU', label: 'country.russia', phone: '+7' },
|
||||||
|
{ code: 'RW', label: 'country.rwanda', phone: '+250' },
|
||||||
|
{ code: 'BL', label: 'country.saint_barthelemy', phone: '+590' },
|
||||||
|
{ code: 'SH', label: 'country.saint_helena', phone: '+290' },
|
||||||
|
{ code: 'KN', label: 'country.saint_kitts_and_nevis', phone: '+1869' },
|
||||||
|
{ code: 'LC', label: 'country.saint_lucia', phone: '+1758' },
|
||||||
|
{ code: 'MF', label: 'country.saint_martin', phone: '+590' },
|
||||||
|
{ code: 'PM', label: 'country.saint_pierre_and_miquelon', phone: '+508' },
|
||||||
|
{
|
||||||
|
code: 'VC',
|
||||||
|
label: 'country.saint_vincent_and_the_grenadines',
|
||||||
|
phone: '+1784',
|
||||||
|
},
|
||||||
|
{ code: 'WS', label: 'country.samoa', phone: '+685' },
|
||||||
|
{ code: 'SM', label: 'country.san_marino', phone: '+378' },
|
||||||
|
{ code: 'ST', label: 'country.sao_tome_and_principe', phone: '+239' },
|
||||||
|
{ code: 'SA', label: 'country.saudi_arabia', phone: '+966' },
|
||||||
|
{ code: 'SN', label: 'country.senegal', phone: '+221' },
|
||||||
|
{ code: 'RS', label: 'country.serbia', phone: '+381' },
|
||||||
|
{ code: 'SC', label: 'country.seychelles', phone: '+248' },
|
||||||
|
{ code: 'SL', label: 'country.sierra_leone', phone: '+232' },
|
||||||
|
{ code: 'SG', label: 'country.singapore', phone: '+65' },
|
||||||
|
{ code: 'SX', label: 'country.sint_maarten', phone: '+1721' },
|
||||||
|
{ code: 'SK', label: 'country.slovakia', phone: '+421' },
|
||||||
|
{ code: 'SI', label: 'country.slovenia', phone: '+386' },
|
||||||
|
{ code: 'SB', label: 'country.solomon_islands', phone: '+677' },
|
||||||
|
{ code: 'SO', label: 'country.somalia', phone: '+252' },
|
||||||
|
{ code: 'ZA', label: 'country.south_africa', phone: '+27' },
|
||||||
|
{
|
||||||
|
code: 'GS',
|
||||||
|
label: 'country.south_georgia_and_south_sandwich_islands',
|
||||||
|
phone: '+500',
|
||||||
|
},
|
||||||
|
{ code: 'KR', label: 'country.south_korea', phone: '+82' },
|
||||||
|
{ code: 'SS', label: 'country.south_sudan', phone: '+211' },
|
||||||
|
{ code: 'ES', label: 'country.spain', phone: '+34' },
|
||||||
|
{ code: 'LK', label: 'country.sri_lanka', phone: '+94' },
|
||||||
|
{ code: 'SD', label: 'country.sudan', phone: '+249' },
|
||||||
|
{ code: 'SR', label: 'country.suriname', phone: '+597' },
|
||||||
|
{ code: 'SJ', label: 'country.svalbard_and_jan_mayen', phone: '+47' },
|
||||||
|
{ code: 'SE', label: 'country.sweden', phone: '+46' },
|
||||||
|
{ code: 'CH', label: 'country.switzerland', phone: '+41' },
|
||||||
|
{ code: 'SY', label: 'country.syria', phone: '+963' },
|
||||||
|
{ code: 'TW', label: 'country.taiwan', phone: '+886' },
|
||||||
|
{ code: 'TJ', label: 'country.tajikistan', phone: '+992' },
|
||||||
|
{ code: 'TZ', label: 'country.tanzania', phone: '+255' },
|
||||||
|
{ code: 'TH', label: 'country.thailand', phone: '+66' },
|
||||||
|
{ code: 'TL', label: 'country.timor_leste', phone: '+670' },
|
||||||
|
{ code: 'TG', label: 'country.togo', phone: '+228' },
|
||||||
|
{ code: 'TK', label: 'country.tokelau', phone: '+690' },
|
||||||
|
{ code: 'TO', label: 'country.tonga', phone: '+676' },
|
||||||
|
{ code: 'TT', label: 'country.trinidad_and_tobago', phone: '+1868' },
|
||||||
|
{ code: 'TN', label: 'country.tunisia', phone: '+216' },
|
||||||
|
{ code: 'TR', label: 'country.turkey', phone: '+90' },
|
||||||
|
{ code: 'TM', label: 'country.turkmenistan', phone: '+993' },
|
||||||
|
{ code: 'TC', label: 'country.turks_and_caicos_islands', phone: '+1649' },
|
||||||
|
{ code: 'TV', label: 'country.tuvalu', phone: '+688' },
|
||||||
|
{ code: 'VI', label: 'country.us_virgin_islands', phone: '+1340' },
|
||||||
|
{ code: 'UG', label: 'country.uganda', phone: '+256' },
|
||||||
|
{ code: 'UA', label: 'country.ukraine', phone: '+380' },
|
||||||
|
{ code: 'AE', label: 'country.united_arab_emirates', phone: '+971' },
|
||||||
|
{ code: 'GB', label: 'country.united_kingdom', phone: '+44' },
|
||||||
|
{ code: 'US', label: 'country.united_states', phone: '+1' },
|
||||||
|
{ code: 'UY', label: 'country.uruguay', phone: '+598' },
|
||||||
|
{ code: 'UZ', label: 'country.uzbekistan', phone: '+998' },
|
||||||
|
{ code: 'VU', label: 'country.vanuatu', phone: '+678' },
|
||||||
|
{ code: 'VA', label: 'country.vatican_city', phone: '+39' },
|
||||||
|
{ code: 'VE', label: 'country.venezuela', phone: '+58' },
|
||||||
|
{ code: 'VN', label: 'country.vietnam', phone: '+84' },
|
||||||
|
{ code: 'WF', label: 'country.wallis_and_futuna', phone: '+681' },
|
||||||
|
{ code: 'EH', label: 'country.western_sahara', phone: '+212' },
|
||||||
|
{ code: 'YE', label: 'country.yemen', phone: '+967' },
|
||||||
|
{ code: 'ZM', label: 'country.zambia', phone: '+260' },
|
||||||
|
{ code: 'ZW', label: 'country.zimbabwe', phone: '+263' },
|
||||||
|
];
|
||||||
104
src/features/authorization/api/authorizationAPI.ts
Normal file
104
src/features/authorization/api/authorizationAPI.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import type { ApiResponse } from '@/types/apiResponse';
|
||||||
|
import type { FetchPromise } from '@/types/fetchPromise';
|
||||||
|
import type {
|
||||||
|
CompleteUserInformationRequest,
|
||||||
|
ConfirmEmailOtpRequest,
|
||||||
|
ConfirmForgetPassCodeRequest,
|
||||||
|
ConfirmOtpResponse,
|
||||||
|
ConfirmSmsOtpRequest,
|
||||||
|
GetUserStatusByPhoneNumberOrEmailRequest,
|
||||||
|
GetUserStatusByPhoneNumberOrEmailResponse,
|
||||||
|
LoginOrSignUpWithGoogleRequest,
|
||||||
|
LoginOrSignUpWithGoogleResponse,
|
||||||
|
LoginRequest,
|
||||||
|
LoginResponse,
|
||||||
|
PasswordLoginRequest,
|
||||||
|
ResetPasswordRequest,
|
||||||
|
ResetPasswordResponse,
|
||||||
|
SendEmailOtpRequest,
|
||||||
|
SendForgetPassCodeRequest,
|
||||||
|
SendSmsOtpRequest,
|
||||||
|
} from '../types/userTypes';
|
||||||
|
|
||||||
|
const API_URL = 'https://accounts.business-harmony.com/api';
|
||||||
|
|
||||||
|
export const fetchRequest = <T = ApiResponse>(
|
||||||
|
url: string,
|
||||||
|
body: Object | null,
|
||||||
|
): FetchPromise<T> => {
|
||||||
|
return fetch(`${API_URL}/${url}`, {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// GetUserStatusByPhoneNumberOrEmail
|
||||||
|
|
||||||
|
export const getUserStatusByPhoneNumberOrEmail = async (
|
||||||
|
body: GetUserStatusByPhoneNumberOrEmailRequest,
|
||||||
|
) => {
|
||||||
|
return fetchRequest<GetUserStatusByPhoneNumberOrEmailResponse>(
|
||||||
|
'User/GetUserStatusByPhoneNumberOrEmail',
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginOrSignUpWithOtp = async (body: LoginRequest) => {
|
||||||
|
return fetchRequest<LoginResponse>('User/LoginOrSignUpWithOtp', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginWithPassword = async (body: PasswordLoginRequest) => {
|
||||||
|
return fetchRequest<LoginResponse>('User/LoginWithPassword', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendSmsOtp = async (body: SendSmsOtpRequest) => {
|
||||||
|
return fetchRequest<ApiResponse>('User/SendSmsOtp', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendEmailOtp = async (body: SendEmailOtpRequest) => {
|
||||||
|
return fetchRequest<ApiResponse>('User/SendEmailOtp', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => {
|
||||||
|
return fetchRequest<ConfirmOtpResponse>('User/ConfirmSmsOtp', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => {
|
||||||
|
return fetchRequest<ConfirmOtpResponse>('User/ConfirmEmailOtp', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetPassword = async (body: ResetPasswordRequest) => {
|
||||||
|
return fetchRequest<ResetPasswordResponse>('User/ResetPassword', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => {
|
||||||
|
return fetchRequest<ApiResponse>('User/SendForgetPassCode', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const confirmForgetPassCode = async (
|
||||||
|
body: ConfirmForgetPassCodeRequest,
|
||||||
|
) => {
|
||||||
|
return fetchRequest<ConfirmOtpResponse>('User/ConfirmForgetPassCode', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginOrSignUpWithGoogle = async (
|
||||||
|
body: LoginOrSignUpWithGoogleRequest,
|
||||||
|
) => {
|
||||||
|
return fetchRequest<LoginOrSignUpWithGoogleResponse>(
|
||||||
|
'User/LoginOrSignUpWithGoogle',
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const completeUserInformation = async (
|
||||||
|
body: CompleteUserInformationRequest,
|
||||||
|
) => {
|
||||||
|
return fetchRequest<ApiResponse>('User/CompleteUserInformation', body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logOut = async () => {
|
||||||
|
return fetchRequest<ApiResponse>('User/LogOut', {});
|
||||||
|
};
|
||||||
23
src/features/authorization/components/AuthenticationCard.tsx
Normal file
23
src/features/authorization/components/AuthenticationCard.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Paper } from '@mui/material';
|
||||||
|
import { type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
// Beacuse in the otp verify there is a element outside of the authentication card
|
||||||
|
export const AuthenticationCard = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: {
|
||||||
|
xs: 4,
|
||||||
|
md: 6,
|
||||||
|
},
|
||||||
|
marginInline: 2,
|
||||||
|
width: (t) => `calc(100% - ${t.spacing(2)})`,
|
||||||
|
maxWidth: '552px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState, type JSX } from 'react';
|
||||||
|
import { LoginRegisterForm } from './LoginRegiserForm';
|
||||||
|
import type { AuthMode, AuthStep, AuthType } from '../../types/authTypes';
|
||||||
|
import { OtpVerifyForm } from './OtpVerifyForm';
|
||||||
|
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||||
|
import { CompleteSignUp } from './CompleteSignUp';
|
||||||
|
import { EnterPasswordForm } from './EnterPasswordForm';
|
||||||
|
import { UserStatus } from '../../types/userTypes';
|
||||||
|
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||||
|
import { VerifyPhoneNumber } from './VerifyPhoneNumber';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AuthenticationSteps = (): JSX.Element => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const DEFAULT_RETURN_URL = '/profile';
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const authReturnUrl: string =
|
||||||
|
searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL;
|
||||||
|
const [authMode, setAuthMode] = useState<AuthMode>('register');
|
||||||
|
const [authType, setAuthType] = useState<AuthType>('phone');
|
||||||
|
const [currentStep, setCurrentStep] = useState<AuthStep>('emailOrPhone');
|
||||||
|
const [loginRegisterValue, setLoginRegisterValue] = useState<string>('');
|
||||||
|
const [countryCode, setCountryCode] = useState<CountryCode>('+98');
|
||||||
|
const [addPhoneCountryCode, setAddPhoneCountryCode] =
|
||||||
|
useState<CountryCode>('+98');
|
||||||
|
const [addedPhoneNumberValue, setAddedPhoneNumberValue] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
|
const handleLoginRegister = (value: string, userStatus: UserStatus) => {
|
||||||
|
setAuthType(isNumeric(value) ? 'phone' : 'email');
|
||||||
|
|
||||||
|
switch (userStatus) {
|
||||||
|
case UserStatus.NotRegistered:
|
||||||
|
setAuthMode('register');
|
||||||
|
setCurrentStep('verify');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserStatus.RegisteredWithoutPassword:
|
||||||
|
setAuthMode('login');
|
||||||
|
setCurrentStep('verify');
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UserStatus.RegisteredWithPassword:
|
||||||
|
setAuthMode('login');
|
||||||
|
setCurrentStep('enterPassword');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserLoggedIn = (userId: GUID) => {
|
||||||
|
localStorage.setItem('userID', userId);
|
||||||
|
|
||||||
|
redirectToReturnUrl();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfrimPhoneNumber = (userId: GUID) => {
|
||||||
|
localStorage.setItem('userID', userId);
|
||||||
|
|
||||||
|
setCurrentStep('addPhoneNumber');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneNumberVerified = () => {
|
||||||
|
redirectToReturnUrl();
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToReturnUrl = () => {
|
||||||
|
if (authReturnUrl === DEFAULT_RETURN_URL) {
|
||||||
|
navigate(DEFAULT_RETURN_URL);
|
||||||
|
} else {
|
||||||
|
location.href = authReturnUrl;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{currentStep === 'emailOrPhone' && (
|
||||||
|
<LoginRegisterForm
|
||||||
|
authReturnUrl={authReturnUrl}
|
||||||
|
onGoogleAuthenticated={handleUserLoggedIn}
|
||||||
|
countryCode={countryCode}
|
||||||
|
setCountryCode={setCountryCode}
|
||||||
|
loginRegisterValue={loginRegisterValue}
|
||||||
|
setLoginRegisterValue={setLoginRegisterValue}
|
||||||
|
authType={authType}
|
||||||
|
setAuthType={setAuthType}
|
||||||
|
onLoginRegisterSubmit={handleLoginRegister}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 'verify' && (
|
||||||
|
<OtpVerifyForm
|
||||||
|
onVerifyPhoneNumber={handleConfrimPhoneNumber}
|
||||||
|
authReturnUrl={authReturnUrl}
|
||||||
|
countryCode={countryCode}
|
||||||
|
onOTPVerified={handleUserLoggedIn}
|
||||||
|
onEditValue={() => setCurrentStep('emailOrPhone')}
|
||||||
|
authMode={authMode}
|
||||||
|
authType={authType}
|
||||||
|
value={loginRegisterValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 'enterPassword' && (
|
||||||
|
<EnterPasswordForm
|
||||||
|
authReturnUrl={authReturnUrl}
|
||||||
|
loginRegisterValue={loginRegisterValue}
|
||||||
|
countryCode={countryCode}
|
||||||
|
authType={authType}
|
||||||
|
onLoggedIn={handleUserLoggedIn}
|
||||||
|
onEditValue={() => setCurrentStep('emailOrPhone')}
|
||||||
|
onLoginWithOTP={() => setCurrentStep('verify')}
|
||||||
|
emailOrPhone={loginRegisterValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 'addPhoneNumber' && (
|
||||||
|
<CompleteSignUp
|
||||||
|
countryCode={addPhoneCountryCode}
|
||||||
|
setCountryCode={setAddPhoneCountryCode}
|
||||||
|
value={addedPhoneNumberValue}
|
||||||
|
setValue={setAddedPhoneNumberValue}
|
||||||
|
email={loginRegisterValue}
|
||||||
|
onCompleteSignUp={() => setCurrentStep('addedPhoneNumberVerify')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 'addedPhoneNumberVerify' && (
|
||||||
|
<VerifyPhoneNumber
|
||||||
|
countryCode={countryCode}
|
||||||
|
onEditValue={() => setCurrentStep('addPhoneNumber')}
|
||||||
|
value={addedPhoneNumberValue}
|
||||||
|
onPhoneNumberVerified={handlePhoneNumberVerified}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { Button, TextField, Typography } from '@mui/material';
|
||||||
|
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||||
|
import { useRef, useState, type Dispatch } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||||
|
import { sendSmsOtp } from '../../api/authorizationAPI';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
export interface CompleteSignUpProps {
|
||||||
|
email: string;
|
||||||
|
value: string;
|
||||||
|
setValue: Dispatch<string>;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
setCountryCode: Dispatch<CountryCode>;
|
||||||
|
onCompleteSignUp: (countryCode: string, value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompleteSignUp = ({
|
||||||
|
email,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
countryCode,
|
||||||
|
setCountryCode,
|
||||||
|
onCompleteSignUp,
|
||||||
|
}: CompleteSignUpProps) => {
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [touched, setTouched] = useState<boolean>(false);
|
||||||
|
const inputError: boolean = touched && !!error;
|
||||||
|
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const isPhoneValid = (code: string, phone: string) => {
|
||||||
|
const phoneNumber = parsePhoneNumberFromString(code + phone);
|
||||||
|
|
||||||
|
return phoneNumber && phoneNumber.isValid();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setTouched(true);
|
||||||
|
|
||||||
|
handleValueError();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValueError = () => {
|
||||||
|
if (!value) {
|
||||||
|
setError(t('loginForm.thisFieldIsRequired'));
|
||||||
|
}
|
||||||
|
if (!isPhoneValid(countryCode, value)) {
|
||||||
|
setError(t('loginForm.phoneNumberIsInvalid'));
|
||||||
|
} else {
|
||||||
|
setError(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompleteSignUp = async () => {
|
||||||
|
handleValueError();
|
||||||
|
|
||||||
|
if (!value || !isPhoneValid(countryCode, value)) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
setSendOtpLoading(true);
|
||||||
|
|
||||||
|
await sendSmsOtp({ phoneNumber: countryCode + value });
|
||||||
|
onCompleteSignUp(countryCode, value);
|
||||||
|
|
||||||
|
setSendOtpLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||||
|
{t('completeSignUp.completeSignUp')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||||
|
{t(
|
||||||
|
'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
|
||||||
|
{ email },
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
ref={textFieldRef}
|
||||||
|
inputRef={inputRef}
|
||||||
|
label={t('completeSignUp.phoneNumber')}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
error={inputError}
|
||||||
|
helperText={inputError ? error : ''}
|
||||||
|
autoFocus
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<CountryCodeSelector
|
||||||
|
value={countryCode}
|
||||||
|
onChange={setCountryCode}
|
||||||
|
show={true}
|
||||||
|
menuAnchor={textFieldRef.current}
|
||||||
|
onCloseFocusRef={inputRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ my: 4 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button loading={sendOtpLoading} onClick={handleCompleteSignUp}>
|
||||||
|
{t('verify.confirmAndContinue')}
|
||||||
|
</Button>
|
||||||
|
</AuthenticationCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import type { AuthType } from '../../types/authTypes';
|
||||||
|
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||||
|
import {
|
||||||
|
loginWithPassword,
|
||||||
|
sendEmailOtp,
|
||||||
|
sendSmsOtp,
|
||||||
|
} from '../../api/authorizationAPI';
|
||||||
|
import type { PasswordLoginRequest } from '../../types/userTypes';
|
||||||
|
|
||||||
|
export interface EnterPasswordFormProps {
|
||||||
|
onEditValue: () => void;
|
||||||
|
onLoginWithOTP: () => void;
|
||||||
|
onLoggedIn: (userId: GUID) => void;
|
||||||
|
emailOrPhone: string;
|
||||||
|
authType: AuthType;
|
||||||
|
loginRegisterValue: string;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
authReturnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnterPasswordForm = ({
|
||||||
|
onEditValue,
|
||||||
|
onLoginWithOTP,
|
||||||
|
onLoggedIn,
|
||||||
|
emailOrPhone,
|
||||||
|
authType,
|
||||||
|
loginRegisterValue,
|
||||||
|
countryCode,
|
||||||
|
authReturnUrl,
|
||||||
|
}: EnterPasswordFormProps) => {
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [passValue, setPassValue] = useState<string>('');
|
||||||
|
const [inputTouched, setInputTouched] = useState<boolean>(false);
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [loginLoading, setLoginLoading] = useState<boolean>(false);
|
||||||
|
const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState<boolean>();
|
||||||
|
const [loginAlertOpen, setLoginAlertOpen] = useState<boolean>(false);
|
||||||
|
const [loginFailedMessage, setLoginFailedMessage] = useState<string>('');
|
||||||
|
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setInputTouched(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!passValue) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
setLoginLoading(true);
|
||||||
|
|
||||||
|
const apiRequest: PasswordLoginRequest = {
|
||||||
|
phoneNumber:
|
||||||
|
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
|
||||||
|
email: authType === 'email' ? loginRegisterValue : undefined,
|
||||||
|
password: passValue,
|
||||||
|
returnUrl: authReturnUrl,
|
||||||
|
};
|
||||||
|
const result = await loginWithPassword(apiRequest);
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (jsonRes.success) {
|
||||||
|
setIsLoginStatusSuccess(true);
|
||||||
|
onLoggedIn(jsonRes.userId);
|
||||||
|
} else {
|
||||||
|
setIsLoginStatusSuccess(false);
|
||||||
|
setLoginFailedMessage(jsonRes.message);
|
||||||
|
}
|
||||||
|
setLoginAlertOpen(true);
|
||||||
|
setLoginLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginWithOtp = async () => {
|
||||||
|
setSendOtpLoading(true);
|
||||||
|
|
||||||
|
if (authType === 'phone') {
|
||||||
|
await sendSmsOtp({ phoneNumber: countryCode + loginRegisterValue });
|
||||||
|
} else {
|
||||||
|
await sendEmailOtp({ email: loginRegisterValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
setSendOtpLoading(false);
|
||||||
|
onLoginWithOTP();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
open={loginAlertOpen}
|
||||||
|
onClose={() => setLoginAlertOpen(false)}
|
||||||
|
color={!isLoginStatusSuccess ? 'error' : 'success'}
|
||||||
|
>
|
||||||
|
{!isLoginStatusSuccess
|
||||||
|
? loginFailedMessage
|
||||||
|
: t('verify.youHaveSuccessfullyLoggedIn')}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 4,
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{t('enterPassword.loginWithPassword')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
sx={{ width: 'auto' }}
|
||||||
|
endIcon={<Edit2 />}
|
||||||
|
onClick={onEditValue}
|
||||||
|
>
|
||||||
|
{emailOrPhone}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||||
|
{t('enterPassword.enterThePasswordYouSetForYourAccount')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t('enterPassword.loginPassword')}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={passValue}
|
||||||
|
inputRef={inputRef}
|
||||||
|
onChange={(e) => setPassValue(e.target.value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
error={!passValue && inputTouched}
|
||||||
|
helperText={
|
||||||
|
!passValue && inputTouched ? t('loginForm.thisFieldIsRequired') : ''
|
||||||
|
}
|
||||||
|
autoFocus
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { sx: { lineHeight: 1.5 } },
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? <Eye /> : <EyeSlash />}
|
||||||
|
</IconButton>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ my: 4 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleLoginWithOtp}
|
||||||
|
sx={{ width: 'auto', mb: 2 }}
|
||||||
|
variant="text"
|
||||||
|
loading={sendOtpLoading}
|
||||||
|
endIcon={<ArrowLeft />}
|
||||||
|
>
|
||||||
|
{t('enterPassword.loginWithOneTimeCode')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Button loading={loginLoading} onClick={handleSubmit}>
|
||||||
|
{t('verify.confirmAndLogin')}
|
||||||
|
</Button>
|
||||||
|
<Link to="/forget-password">
|
||||||
|
<Button variant="text">{t('enterPassword.iForgotMyPassword')}</Button>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
</AuthenticationCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { GoogleCodeClientResponse } from '../../types/userTypes';
|
||||||
|
import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
|
||||||
|
import type { GUID } from '@/types/commonTypes';
|
||||||
|
import { Google } from 'iconsax-react';
|
||||||
|
|
||||||
|
export interface GoogleAuthenticationProps {
|
||||||
|
disabled: boolean;
|
||||||
|
authReturnUrl: string;
|
||||||
|
onGoogleAuthenticated: (userId: GUID) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GoogleAuthentication = ({
|
||||||
|
disabled,
|
||||||
|
authReturnUrl,
|
||||||
|
onGoogleAuthenticated,
|
||||||
|
}: GoogleAuthenticationProps) => {
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [loginWithGoogleLoading, setLoginWithGoogleLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
|
const clientRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://accounts.google.com/gsi/client';
|
||||||
|
script.async = true;
|
||||||
|
script.defer = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
clientRef.current = google.accounts.oauth2.initCodeClient({
|
||||||
|
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
|
||||||
|
scope: 'openid email profile',
|
||||||
|
ux_mode: 'popup',
|
||||||
|
response_type: 'id_token',
|
||||||
|
callback: async (resp: GoogleCodeClientResponse) => {
|
||||||
|
setLoginWithGoogleLoading(true);
|
||||||
|
|
||||||
|
const result = await loginOrSignUpWithGoogle({
|
||||||
|
idToken: resp.id_token,
|
||||||
|
returnUrl: authReturnUrl,
|
||||||
|
});
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (jsonRes.success) {
|
||||||
|
onGoogleAuthenticated(jsonRes.userId);
|
||||||
|
} else {
|
||||||
|
// Todo: Add useToast to handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoginWithGoogleLoading(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.removeChild(script);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGoogleLogin = () => {
|
||||||
|
if (clientRef.current) {
|
||||||
|
clientRef.current.requestCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={handleGoogleLogin}
|
||||||
|
disabled={disabled}
|
||||||
|
loading={loginWithGoogleLoading}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Google variant="Bold" />}
|
||||||
|
>
|
||||||
|
{t('loginForm.loginWithGoogle')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { Button, Stack, TextField, Typography } from '@mui/material';
|
||||||
|
import { useRef, useState, type Dispatch } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||||
|
import type { AuthType } from '../../types/authTypes';
|
||||||
|
import { isEmail } from '@/utils/regexes/isEmail';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||||
|
import type { UserStatus } from '../../types/userTypes';
|
||||||
|
import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||||
|
import { GoogleAuthentication } from './GoogleAuthentication';
|
||||||
|
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
|
||||||
|
|
||||||
|
export interface LoginRegisterFormProps {
|
||||||
|
loginRegisterValue: string;
|
||||||
|
setLoginRegisterValue: Dispatch<string>;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
setCountryCode: Dispatch<CountryCode>;
|
||||||
|
authType: AuthType;
|
||||||
|
setAuthType: Dispatch<AuthType>;
|
||||||
|
onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void;
|
||||||
|
authReturnUrl: string;
|
||||||
|
onGoogleAuthenticated: (userId: GUID) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginRegisterForm({
|
||||||
|
loginRegisterValue,
|
||||||
|
setLoginRegisterValue,
|
||||||
|
countryCode,
|
||||||
|
setCountryCode,
|
||||||
|
authType,
|
||||||
|
setAuthType,
|
||||||
|
onLoginRegisterSubmit,
|
||||||
|
authReturnUrl,
|
||||||
|
onGoogleAuthenticated,
|
||||||
|
}: LoginRegisterFormProps) {
|
||||||
|
const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false);
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const [touched, setTouched] = useState<boolean>(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const inputError: boolean = touched && !!error;
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
setLoginRegisterValue(newValue);
|
||||||
|
|
||||||
|
// If the new value contains only digits (or is empty), it's a phone number
|
||||||
|
if (isNumeric(newValue)) {
|
||||||
|
setAuthType('phone');
|
||||||
|
} else {
|
||||||
|
setAuthType('email');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setTouched(true);
|
||||||
|
validateInput(loginRegisterValue, authType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInput = (
|
||||||
|
value: string,
|
||||||
|
authType: AuthType,
|
||||||
|
setErrors: boolean = true,
|
||||||
|
): boolean => {
|
||||||
|
if (!value) {
|
||||||
|
if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === 'email' && !isEmail(value)) {
|
||||||
|
if (setErrors) setError(t('loginForm.emailIsInvalid'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
|
||||||
|
if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setErrors) setError(undefined);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (validateInput(loginRegisterValue, authType, false)) {
|
||||||
|
setCheckStatusLoading(true);
|
||||||
|
const result = await getUserStatusByPhoneNumberOrEmail({
|
||||||
|
phoneNumber:
|
||||||
|
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
|
||||||
|
email: authType === 'email' ? loginRegisterValue : undefined,
|
||||||
|
});
|
||||||
|
const jsonResult = await result.json();
|
||||||
|
|
||||||
|
if (jsonResult.success) {
|
||||||
|
onLoginRegisterSubmit(loginRegisterValue, jsonResult.userStatus);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(jsonResult.message);
|
||||||
|
}
|
||||||
|
setCheckStatusLoading(false);
|
||||||
|
} else {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
validateInput(loginRegisterValue, authType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
color="error"
|
||||||
|
onClose={() => setErrorMessage(undefined)}
|
||||||
|
open={!!errorMessage}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="h5">{t('loginForm.title')}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t('loginForm.description')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
ref={textFieldRef}
|
||||||
|
inputRef={inputRef}
|
||||||
|
label={t('loginForm.emailOrPhoneLabel')}
|
||||||
|
value={loginRegisterValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
error={inputError}
|
||||||
|
helperText={inputError ? error : ''}
|
||||||
|
autoFocus
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<CountryCodeSelector
|
||||||
|
value={countryCode}
|
||||||
|
onChange={setCountryCode}
|
||||||
|
show={showAdornment}
|
||||||
|
menuAnchor={textFieldRef.current}
|
||||||
|
onCloseFocusRef={inputRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ my: 4 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Button loading={checkStatusLoading} onClick={handleSubmit}>
|
||||||
|
{t('loginForm.submitButton')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<GoogleAuthentication
|
||||||
|
authReturnUrl={authReturnUrl}
|
||||||
|
onGoogleAuthenticated={onGoogleAuthenticated}
|
||||||
|
disabled={checkStatusLoading}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</AuthenticationCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||||
|
import { Edit2 } from 'iconsax-react';
|
||||||
|
import DigitInput from '@/components/components/DigitsInput';
|
||||||
|
import type { AuthMode, AuthType } from '../../types/authTypes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import type { LoginRequest } from '../../types/userTypes';
|
||||||
|
import {
|
||||||
|
loginOrSignUpWithOtp,
|
||||||
|
sendEmailOtp,
|
||||||
|
sendSmsOtp,
|
||||||
|
} from '../../api/authorizationAPI';
|
||||||
|
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
interface OtpVerifyFormProps {
|
||||||
|
value: string;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
authType: AuthType;
|
||||||
|
authMode: AuthMode;
|
||||||
|
onEditValue: () => void;
|
||||||
|
onOTPVerified: (userId: GUID) => void;
|
||||||
|
onVerifyPhoneNumber: (userId: GUID) => void;
|
||||||
|
authReturnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OtpVerifyForm({
|
||||||
|
value,
|
||||||
|
countryCode,
|
||||||
|
authType,
|
||||||
|
authMode,
|
||||||
|
onEditValue,
|
||||||
|
onOTPVerified,
|
||||||
|
onVerifyPhoneNumber,
|
||||||
|
authReturnUrl,
|
||||||
|
}: OtpVerifyFormProps) {
|
||||||
|
const [otpCode, setOtpCode] = useState<string>('');
|
||||||
|
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||||
|
const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||||
|
const [canResend, setCanResend] = useState(false);
|
||||||
|
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
if (resendTimer > 0) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setResendTimer((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
setCanResend(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [resendTimer]);
|
||||||
|
|
||||||
|
const handleResendOTPCode = async () => {
|
||||||
|
setResendLoading(true);
|
||||||
|
|
||||||
|
if (authType === 'phone') {
|
||||||
|
await sendSmsOtp({ phoneNumber: countryCode + value });
|
||||||
|
} else {
|
||||||
|
await sendEmailOtp({ email: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
setResendTimer(120);
|
||||||
|
setCanResend(false);
|
||||||
|
setResendLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const min = Math.floor(seconds / 60);
|
||||||
|
const sec = seconds % 60;
|
||||||
|
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOTP = () => {
|
||||||
|
if (!otpCode || otpCode.length < 4) {
|
||||||
|
setOtpDigitInvalid(true);
|
||||||
|
} else {
|
||||||
|
handleLoginOrSignUp();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoginOrSignUp = async () => {
|
||||||
|
setOtpDigitInvalid(false);
|
||||||
|
setVerifyStatusLoading(true);
|
||||||
|
|
||||||
|
const loginRequest: LoginRequest = {
|
||||||
|
otpCode: otpCode,
|
||||||
|
phoneNumber: authType === 'phone' ? countryCode + value : undefined,
|
||||||
|
email: authType === 'email' ? value : undefined,
|
||||||
|
returnUrl: authReturnUrl,
|
||||||
|
};
|
||||||
|
const result = await loginOrSignUpWithOtp(loginRequest);
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (jsonRes.success) {
|
||||||
|
setVerifyStatus('success');
|
||||||
|
|
||||||
|
if (jsonRes.registeredWithOutPhoneNumber) {
|
||||||
|
onVerifyPhoneNumber(jsonRes.userId);
|
||||||
|
} else {
|
||||||
|
onOTPVerified(jsonRes.userId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setVerifyStatus('failed');
|
||||||
|
setErrorMessage(jsonRes.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerifyAlertOpen(true);
|
||||||
|
setVerifyStatusLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const otpMessage = (): string => {
|
||||||
|
if (authType === 'phone' && authMode === 'login') {
|
||||||
|
return t(
|
||||||
|
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
|
||||||
|
);
|
||||||
|
} else if (authType === 'phone' && authMode === 'register') {
|
||||||
|
return t(
|
||||||
|
'verify.thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount',
|
||||||
|
);
|
||||||
|
} else if (authType === 'email' && authMode === 'login') {
|
||||||
|
return t(
|
||||||
|
'verify.a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt',
|
||||||
|
);
|
||||||
|
} else if (authType === 'email' && authMode === 'register') {
|
||||||
|
return t(
|
||||||
|
'verify.thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const toastMessage =
|
||||||
|
verifyStatus === 'failed'
|
||||||
|
? (errorMessage ?? t('verify.theVerificationCodeIsIncorrect'))
|
||||||
|
: verifyStatus === 'success' && authMode === 'register'
|
||||||
|
? t('verify.youHaveSuccessfullySignedIn')
|
||||||
|
: verifyStatus === 'success' && authMode === 'login'
|
||||||
|
? t('verify.youHaveSuccessfullyLoggedIn')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack alignItems="center">
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
open={verifyAlertOpen}
|
||||||
|
onClose={() => setVerifyAlertOpen(false)}
|
||||||
|
color={verifyStatus === 'failed' ? 'error' : 'success'}
|
||||||
|
>
|
||||||
|
{toastMessage}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 4,
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||||
|
endIcon={<Edit2 />}
|
||||||
|
onClick={onEditValue}
|
||||||
|
>
|
||||||
|
{authType === 'phone' ? countryCode + value : value}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
{otpMessage()}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<DigitInput
|
||||||
|
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||||
|
success={verifyStatus === 'success'}
|
||||||
|
onChange={(value) => setOtpCode(value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||||
|
{authMode === 'register'
|
||||||
|
? t('verify.confirmAndContinue')
|
||||||
|
: t('verify.confirmAndLogin')}
|
||||||
|
</Button>
|
||||||
|
</AuthenticationCard>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
mt: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
loading={resendLoading}
|
||||||
|
sx={{ width: 'auto' }}
|
||||||
|
onClick={canResend ? handleResendOTPCode : undefined}
|
||||||
|
>
|
||||||
|
{canResend && t('verify.resendCode')}
|
||||||
|
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||||
|
import { Edit2 } from 'iconsax-react';
|
||||||
|
import DigitInput from '@/components/components/DigitsInput';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import type { ConfirmSmsOtpRequest } from '../../types/userTypes';
|
||||||
|
import { confirmSmsOtp, sendSmsOtp } from '../../api/authorizationAPI';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
interface VerifyPhoneNumberProps {
|
||||||
|
value: string;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
onEditValue: () => void;
|
||||||
|
onPhoneNumberVerified: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerifyPhoneNumber({
|
||||||
|
value,
|
||||||
|
countryCode,
|
||||||
|
onEditValue,
|
||||||
|
onPhoneNumberVerified,
|
||||||
|
}: VerifyPhoneNumberProps) {
|
||||||
|
const [otpCode, setOtpCode] = useState<string>('');
|
||||||
|
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||||
|
const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||||
|
const [canResend, setCanResend] = useState(false);
|
||||||
|
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
if (resendTimer > 0) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setResendTimer((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
setCanResend(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [resendTimer]);
|
||||||
|
|
||||||
|
const handleResendOTPCode = async () => {
|
||||||
|
setResendLoading(true);
|
||||||
|
|
||||||
|
await sendSmsOtp({ phoneNumber: countryCode + value });
|
||||||
|
|
||||||
|
setResendTimer(120);
|
||||||
|
setCanResend(false);
|
||||||
|
setResendLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const min = Math.floor(seconds / 60);
|
||||||
|
const sec = seconds % 60;
|
||||||
|
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOTP = async () => {
|
||||||
|
if (!otpCode || otpCode.length < 4) {
|
||||||
|
setOtpDigitInvalid(true);
|
||||||
|
} else {
|
||||||
|
setOtpDigitInvalid(false);
|
||||||
|
setVerifyStatusLoading(true);
|
||||||
|
|
||||||
|
const confirmSmsOtpRequest: ConfirmSmsOtpRequest = {
|
||||||
|
otpCode: otpCode,
|
||||||
|
phoneNumber: countryCode + value,
|
||||||
|
};
|
||||||
|
const result = await confirmSmsOtp(confirmSmsOtpRequest);
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (jsonRes.success) {
|
||||||
|
setVerifyStatus('success');
|
||||||
|
onPhoneNumberVerified();
|
||||||
|
} else {
|
||||||
|
setVerifyStatus('failed');
|
||||||
|
setErrorMessage(jsonRes.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerifyAlertOpen(true);
|
||||||
|
setVerifyStatusLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyAlertMessage = (): string => {
|
||||||
|
if (verifyStatus === 'failed') {
|
||||||
|
return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
|
||||||
|
} else {
|
||||||
|
return t('verify.youHaveSuccessfullyLoggedIn');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack alignItems="center">
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
open={verifyAlertOpen}
|
||||||
|
onClose={() => setVerifyAlertOpen(false)}
|
||||||
|
color={verifyStatus === 'failed' ? 'error' : 'success'}
|
||||||
|
>
|
||||||
|
{verifyAlertMessage()}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 4,
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||||
|
endIcon={<Edit2 />}
|
||||||
|
onClick={onEditValue}
|
||||||
|
>
|
||||||
|
{countryCode + value}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
{t(
|
||||||
|
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<DigitInput
|
||||||
|
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||||
|
success={verifyStatus === 'success'}
|
||||||
|
onChange={(value) => setOtpCode(value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||||
|
{t('verify.confirmAndLogin')}
|
||||||
|
</Button>
|
||||||
|
</AuthenticationCard>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
mt: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
loading={resendLoading}
|
||||||
|
sx={{ width: 'auto' }}
|
||||||
|
onClick={canResend ? handleResendOTPCode : undefined}
|
||||||
|
>
|
||||||
|
{canResend && t('verify.resendCode')}
|
||||||
|
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
254
src/features/authorization/components/CountryCodeSelector.tsx
Normal file
254
src/features/authorization/components/CountryCodeSelector.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
InputAdornment,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useMemo, useRef, useState, type RefObject } from 'react';
|
||||||
|
import { ArrowDown2 } from 'iconsax-react';
|
||||||
|
import ReactCountryFlag from 'react-country-flag';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { countries, type Country } from '../../../countries';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
interface CountryCodeSelectorProps {
|
||||||
|
show: boolean;
|
||||||
|
value: CountryCode;
|
||||||
|
onChange: (newValue: CountryCode) => void;
|
||||||
|
menuAnchor: HTMLElement | null;
|
||||||
|
onCloseFocusRef: RefObject<HTMLInputElement | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An animated country code adornment that fades and slides into view.
|
||||||
|
* Its visibility is controlled by the `show` prop.
|
||||||
|
*/
|
||||||
|
export function CountryCodeSelector({
|
||||||
|
show,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
menuAnchor,
|
||||||
|
onCloseFocusRef,
|
||||||
|
}: CountryCodeSelectorProps) {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const selectedCountry =
|
||||||
|
countries.find((c) => c.phone === value) || countries[0];
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setAnchorEl(menuAnchor);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
}, 0);
|
||||||
|
setTimeout(() => {
|
||||||
|
onCloseFocusRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
setSearchTerm(''); // Reset search on close
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (country: Country) => {
|
||||||
|
onChange(country.phone);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuEntered = () => {
|
||||||
|
// Focus the input field after the menu has finished opening
|
||||||
|
searchInputRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredCountries = useMemo(
|
||||||
|
() =>
|
||||||
|
countries.filter(
|
||||||
|
(country) =>
|
||||||
|
t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
country.phone.includes(searchTerm),
|
||||||
|
),
|
||||||
|
[searchTerm],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputAdornment
|
||||||
|
position={i18n.dir() === 'rtl' ? 'start' : 'end'}
|
||||||
|
sx={{
|
||||||
|
mx: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
onClick={handleClick}
|
||||||
|
sx={{
|
||||||
|
// Animate width and opacity based on the 'show' prop
|
||||||
|
width: show ? 'auto' : 0,
|
||||||
|
opacity: show ? 1 : 0,
|
||||||
|
transition: (theme) =>
|
||||||
|
theme.transitions.create(['width', 'opacity'], {
|
||||||
|
duration: theme.transitions.duration.standard,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Prevent content from wrapping or spilling out during animation
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
|
||||||
|
// layout styles
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.25,
|
||||||
|
pl: show ? 0.25 : 0,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* This inner Box prevents the content from being squeezed during the transition */}
|
||||||
|
<ArrowDown2 size="24" variant="Bold" />
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
color="text.primary"
|
||||||
|
component="div"
|
||||||
|
sx={{ direction: 'rtl' }} // TODO: need to fixed for both en and fa
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ReactCountryFlag
|
||||||
|
countryCode={selectedCountry.code}
|
||||||
|
svg
|
||||||
|
style={{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '1.5rem',
|
||||||
|
// TODO: Check alignment for better styling definition
|
||||||
|
marginTop: '-2px',
|
||||||
|
marginRight: '4px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
slotProps={{
|
||||||
|
list: {
|
||||||
|
sx: {
|
||||||
|
// Set the width to match the anchor element's width
|
||||||
|
width: menuWidth,
|
||||||
|
height: '18.75rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
onEntered: handleMenuEntered,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
p: 1,
|
||||||
|
backgroundColor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
inputRef={searchInputRef}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label={t('labels.search')}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Can improve preformance with using virtual scrolling */}
|
||||||
|
<Box sx={{ width: '100%', maxHeight: '14.75rem', overflow: 'auto' }}>
|
||||||
|
{filteredCountries.length === 0 ? (
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
|
||||||
|
</ListItem>
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
filteredCountries.map((country) => (
|
||||||
|
<MenuItem
|
||||||
|
key={country.code}
|
||||||
|
selected={country.phone === value}
|
||||||
|
onClick={() => handleSelect(country)}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<ReactCountryFlag
|
||||||
|
countryCode={country.code}
|
||||||
|
svg
|
||||||
|
style={{
|
||||||
|
height: '1.125rem',
|
||||||
|
width: '1.125rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={t(country.label)} />
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{country.phone}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* virtual scrolling */}
|
||||||
|
{/* <Virtuoso
|
||||||
|
style={{ height: '14.75rem' }} // Adjust height to account for the search bar
|
||||||
|
data={filteredCountries}
|
||||||
|
components={{
|
||||||
|
EmptyPlaceholder: () => (
|
||||||
|
<MenuItem disabled>
|
||||||
|
<ListItemText>{t('messages.noResultFound')}</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
initialTopMostItemIndex={countries.indexOf(selectedCountry)}
|
||||||
|
itemContent={(_, country) => (
|
||||||
|
<MenuItem
|
||||||
|
key={country.code}
|
||||||
|
selected={country.phone === value}
|
||||||
|
onClick={() => handleSelect(country)}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<ReactCountryFlag
|
||||||
|
countryCode={country.code}
|
||||||
|
svg
|
||||||
|
style={{ fontSize: '1.25em', lineHeight: '1.25em' }}
|
||||||
|
/>
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={country.label} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{country.phone}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
</InputAdornment>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { containsNumber } from '@/utils/regexes/containsNumber';
|
||||||
|
import { containsSymbol } from '@/utils/regexes/containsSymbol';
|
||||||
|
import { least8Chars } from '@/utils/regexes/least8Chars';
|
||||||
|
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
|
||||||
|
import type { ResetPasswordRequest } from '../../types/userTypes';
|
||||||
|
import type { AuthType } from '../../types/authTypes';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
import { resetPassword } from '../../api/authorizationAPI';
|
||||||
|
|
||||||
|
export interface ChangePasswordProps {
|
||||||
|
onEditInfo: () => void;
|
||||||
|
onPasswordChanged: () => void;
|
||||||
|
forgettedPasswordInfo: string;
|
||||||
|
infoType: AuthType;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangePassword = ({
|
||||||
|
onEditInfo,
|
||||||
|
onPasswordChanged,
|
||||||
|
forgettedPasswordInfo,
|
||||||
|
infoType,
|
||||||
|
countryCode,
|
||||||
|
}: ChangePasswordProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [passValue, setPassValue] = useState<string>('');
|
||||||
|
const [confirmPassValue, setConfirmPassValue] = useState<string>('');
|
||||||
|
const [inputTouched, setInputTouched] = useState<boolean>(false);
|
||||||
|
const [confirmInputTouched, setConfirmInputTouched] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const confirmInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [changePasswordLoading, setChangePasswordLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [changePasswordStatus, setChangePasswordStatus] = useState<
|
||||||
|
'success' | 'failed'
|
||||||
|
>();
|
||||||
|
const [changePassAlertOpen, setChangePassAlertOpen] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [changePassFailedMessage, setChangePassFailedMessage] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
|
const passwordValidationRules = [
|
||||||
|
{ title: t('forgetPassword.includingANumber'), validator: containsNumber },
|
||||||
|
{ title: t('forgetPassword.atLeast8Characters'), validator: least8Chars },
|
||||||
|
{
|
||||||
|
title: t('forgetPassword.containsAnUppercaseAndLowercaseLetter'),
|
||||||
|
validator: hasUpperAndLowerLetter,
|
||||||
|
},
|
||||||
|
{ title: t('forgetPassword.ContainsASymbol'), validator: containsSymbol },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setInputTouched(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmPassBlur = () => {
|
||||||
|
setConfirmInputTouched(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!passValue || !isValidPassword(passValue)) {
|
||||||
|
setInputTouched(true);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
} else if (passValue !== confirmPassValue) {
|
||||||
|
setConfirmInputTouched(true);
|
||||||
|
confirmInputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
setChangePasswordLoading(true);
|
||||||
|
|
||||||
|
const apiRequest: ResetPasswordRequest = {
|
||||||
|
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||||
|
phoneNumber:
|
||||||
|
infoType === 'phone'
|
||||||
|
? countryCode + forgettedPasswordInfo
|
||||||
|
: undefined,
|
||||||
|
newPassword: passValue,
|
||||||
|
confirmNewPassword: confirmPassValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resetPassword(apiRequest);
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (jsonRes.success) {
|
||||||
|
setChangePasswordStatus('success');
|
||||||
|
onPasswordChanged();
|
||||||
|
} else {
|
||||||
|
setChangePasswordStatus('failed');
|
||||||
|
setChangePassFailedMessage(jsonRes.message);
|
||||||
|
}
|
||||||
|
setChangePassAlertOpen(true);
|
||||||
|
|
||||||
|
setChangePasswordLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidPassword = (value: string) => {
|
||||||
|
return (
|
||||||
|
containsNumber(value) &&
|
||||||
|
containsSymbol(value) &&
|
||||||
|
least8Chars(value) &&
|
||||||
|
hasUpperAndLowerLetter(value)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
open={changePassAlertOpen}
|
||||||
|
onClose={() => setChangePassAlertOpen(false)}
|
||||||
|
color={changePasswordStatus === 'failed' ? 'error' : 'success'}
|
||||||
|
>
|
||||||
|
{changePasswordStatus === 'failed'
|
||||||
|
? changePassFailedMessage
|
||||||
|
: t('forgetPassword.passwordChangedSuccessfully')}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 4,
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{t('forgetPassword.changePassword')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||||
|
endIcon={<Edit2 />}
|
||||||
|
onClick={onEditInfo}
|
||||||
|
>
|
||||||
|
{forgettedPasswordInfo}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||||
|
{t('forgetPassword.createANewPassword')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t('forgetPassword.newPassword')}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={passValue}
|
||||||
|
inputRef={inputRef}
|
||||||
|
onChange={(e) => setPassValue(e.target.value)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
error={inputTouched && !isValidPassword(passValue)}
|
||||||
|
autoFocus
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
|
||||||
|
input: {
|
||||||
|
startAdornment: confirmPassValue &&
|
||||||
|
isValidPassword(passValue) &&
|
||||||
|
passValue === confirmPassValue && (
|
||||||
|
<TickCircle variant="Bold" color={theme.palette.success.main} />
|
||||||
|
),
|
||||||
|
endAdornment: passValue ? (
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? <Eye /> : <EyeSlash />}
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ mt: 4 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isValidPassword(passValue) && (
|
||||||
|
<Stack spacing={1} sx={{ mt: 2 }}>
|
||||||
|
{passwordValidationRules.map((rule) => (
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
|
<TickCircle
|
||||||
|
variant={rule.validator(passValue) ? 'Bold' : 'Linear'}
|
||||||
|
color={
|
||||||
|
rule.validator(passValue)
|
||||||
|
? theme.palette.success.main
|
||||||
|
: theme.palette.primary.light
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="body2">{rule.title}</Typography>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label={t('forgetPassword.confirmPassword')}
|
||||||
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
|
value={confirmPassValue}
|
||||||
|
inputRef={confirmInputRef}
|
||||||
|
onChange={(e) => setConfirmPassValue(e.target.value)}
|
||||||
|
onBlur={handleConfirmPassBlur}
|
||||||
|
error={confirmInputTouched && confirmPassValue !== passValue}
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
|
||||||
|
input: {
|
||||||
|
startAdornment: confirmPassValue &&
|
||||||
|
isValidPassword(passValue) &&
|
||||||
|
passValue === confirmPassValue && (
|
||||||
|
<TickCircle variant="Bold" color={theme.palette.success.main} />
|
||||||
|
),
|
||||||
|
endAdornment: confirmPassValue ? (
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? <Eye /> : <EyeSlash />}
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ my: 4 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Button loading={changePasswordLoading} onClick={handleSubmit}>
|
||||||
|
{t('forgetPassword.confirm')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</AuthenticationCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { AuthType } from '../../types/authTypes';
|
||||||
|
import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
|
||||||
|
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
|
||||||
|
import { ChangePassword } from './ChangePassword';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
export const ForgetPasswordContainer = () => {
|
||||||
|
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
|
||||||
|
'enterInfo' | 'verifyOtp' | 'setPassword'
|
||||||
|
>('enterInfo');
|
||||||
|
const [forgettedPasswordInfo, setForgettedPasswordInfo] =
|
||||||
|
useState<string>('');
|
||||||
|
const [infoCountryCode, setInfoCountryCode] = useState<CountryCode>('+98');
|
||||||
|
const [infoType, setInfoType] = useState<AuthType>('email');
|
||||||
|
|
||||||
|
const handleVerifyOtp = () => {
|
||||||
|
setForgetPassCurrentStep('verifyOtp');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditInfo = () => {
|
||||||
|
setForgetPassCurrentStep('enterInfo');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOtpVerified = () => {
|
||||||
|
setForgetPassCurrentStep('setPassword');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChanged = () => {
|
||||||
|
console.log('changingPasswordTo');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{forgetPassCurrentStep === 'enterInfo' && (
|
||||||
|
<ForgettedPasswordInfo
|
||||||
|
infoType={infoType}
|
||||||
|
setInfoType={setInfoType}
|
||||||
|
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||||
|
setForgettedPasswordInfo={setForgettedPasswordInfo}
|
||||||
|
onVerifyOtp={handleVerifyOtp}
|
||||||
|
countryCode={infoCountryCode}
|
||||||
|
setCountryCode={setInfoCountryCode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{forgetPassCurrentStep === 'verifyOtp' && (
|
||||||
|
<ForgetPasswordOtp
|
||||||
|
countryCode={infoCountryCode}
|
||||||
|
infoType={infoType}
|
||||||
|
onEditInfo={handleEditInfo}
|
||||||
|
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||||
|
onOTPVerified={handleOtpVerified}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{forgetPassCurrentStep === 'setPassword' && (
|
||||||
|
<ChangePassword
|
||||||
|
onEditInfo={handleEditInfo}
|
||||||
|
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||||
|
onPasswordChanged={handlePasswordChanged}
|
||||||
|
infoType={infoType}
|
||||||
|
countryCode={infoCountryCode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||||
|
import { Edit2 } from 'iconsax-react';
|
||||||
|
import DigitInput from '@/components/components/DigitsInput';
|
||||||
|
import type { AuthType } from '../../types/authTypes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import type { ConfirmForgetPassCodeRequest } from '../../types/userTypes';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
import { confirmForgetPassCode } from '../../api/authorizationAPI';
|
||||||
|
|
||||||
|
interface ForgetPasswordOtpProps {
|
||||||
|
forgettedPasswordInfo: string;
|
||||||
|
infoType: AuthType;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
onEditInfo: () => void;
|
||||||
|
onOTPVerified: (otpCode: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ForgetPasswordOtp({
|
||||||
|
forgettedPasswordInfo,
|
||||||
|
infoType,
|
||||||
|
countryCode,
|
||||||
|
onEditInfo,
|
||||||
|
onOTPVerified,
|
||||||
|
}: ForgetPasswordOtpProps) {
|
||||||
|
const [otpCode, setOtpCode] = useState<string>('');
|
||||||
|
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||||
|
const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>();
|
||||||
|
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [verifyAlertMessage, setVerifyAlertMessage] = useState<string>();
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||||
|
const [canResend, setCanResend] = useState(false);
|
||||||
|
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
if (resendTimer > 0) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setResendTimer((prev) => prev - 1);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
setCanResend(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [resendTimer]);
|
||||||
|
|
||||||
|
const handleResendOTPCode = () => {
|
||||||
|
setResendLoading(true);
|
||||||
|
|
||||||
|
// TODO: Call API here instead of settimeout
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('resended');
|
||||||
|
|
||||||
|
setResendTimer(120);
|
||||||
|
setCanResend(false);
|
||||||
|
setResendLoading(false);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const min = Math.floor(seconds / 60);
|
||||||
|
const sec = seconds % 60;
|
||||||
|
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyOTP = async () => {
|
||||||
|
if (!otpCode || otpCode.length < 4) {
|
||||||
|
setOtpDigitInvalid(true);
|
||||||
|
} else {
|
||||||
|
setOtpDigitInvalid(false);
|
||||||
|
setVerifyStatusLoading(true);
|
||||||
|
|
||||||
|
// Change setTimeout to api call
|
||||||
|
const apiRequest: ConfirmForgetPassCodeRequest = {
|
||||||
|
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||||
|
phoneNumber:
|
||||||
|
infoType === 'phone'
|
||||||
|
? countryCode + forgettedPasswordInfo
|
||||||
|
: undefined,
|
||||||
|
code: otpCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await confirmForgetPassCode(apiRequest);
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (jsonRes.success) {
|
||||||
|
setVerifyStatus('success');
|
||||||
|
onOTPVerified(otpCode);
|
||||||
|
} else {
|
||||||
|
setVerifyStatus('failed');
|
||||||
|
setVerifyAlertMessage(jsonRes.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerifyStatusLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack alignItems="center">
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
open={!!verifyAlertMessage}
|
||||||
|
onClose={() => setVerifyAlertMessage(undefined)}
|
||||||
|
color={'error'}
|
||||||
|
>
|
||||||
|
{verifyAlertMessage}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 4,
|
||||||
|
mb: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{t('forgetPassword.forgetPassword')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||||
|
endIcon={<Edit2 />}
|
||||||
|
onClick={onEditInfo}
|
||||||
|
>
|
||||||
|
{infoType === 'phone'
|
||||||
|
? countryCode + forgettedPasswordInfo
|
||||||
|
: forgettedPasswordInfo}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||||
|
{infoType === 'email'
|
||||||
|
? t(
|
||||||
|
'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<DigitInput
|
||||||
|
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||||
|
success={verifyStatus === 'success'}
|
||||||
|
onChange={(value) => setOtpCode(value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||||
|
{t('forgetPassword.confirm')}
|
||||||
|
</Button>
|
||||||
|
</AuthenticationCard>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
sx={{
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
mt: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
loading={resendLoading}
|
||||||
|
sx={{ width: 'auto' }}
|
||||||
|
onClick={canResend ? handleResendOTPCode : undefined}
|
||||||
|
>
|
||||||
|
{canResend && t('verify.resendCode')}
|
||||||
|
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { Button, Stack, TextField, Typography } from '@mui/material';
|
||||||
|
import { useRef, useState, type Dispatch } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||||
|
import type { AuthType } from '../../types/authTypes';
|
||||||
|
import { isEmail } from '@/utils/regexes/isEmail';
|
||||||
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
|
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||||
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
|
import { sendForgetPassCode } from '../../api/authorizationAPI';
|
||||||
|
import type { SendForgetPassCodeRequest } from '../../types/userTypes';
|
||||||
|
import { Toast } from '@/components/Toast';
|
||||||
|
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
|
||||||
|
|
||||||
|
export interface ForgettedPasswordInfoProps {
|
||||||
|
forgettedPasswordInfo: string;
|
||||||
|
setForgettedPasswordInfo: Dispatch<string>;
|
||||||
|
infoType: AuthType;
|
||||||
|
setInfoType: Dispatch<AuthType>;
|
||||||
|
onVerifyOtp: (value: string) => void;
|
||||||
|
countryCode: CountryCode;
|
||||||
|
setCountryCode: Dispatch<CountryCode>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ForgettedPasswordInfo({
|
||||||
|
forgettedPasswordInfo,
|
||||||
|
setForgettedPasswordInfo,
|
||||||
|
infoType,
|
||||||
|
setInfoType,
|
||||||
|
onVerifyOtp,
|
||||||
|
countryCode,
|
||||||
|
setCountryCode,
|
||||||
|
}: ForgettedPasswordInfoProps) {
|
||||||
|
const { t } = useTranslation('authentication');
|
||||||
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
const [touched, setTouched] = useState<boolean>(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
const [sendCodeLoading, setSendCodeLoading] = useState<boolean>(false);
|
||||||
|
const inputError: boolean = touched && !!error;
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
setForgettedPasswordInfo(newValue);
|
||||||
|
|
||||||
|
// If the new value contains only digits (or is empty), it's a phone number
|
||||||
|
if (isNumeric(newValue)) {
|
||||||
|
setInfoType('phone');
|
||||||
|
} else {
|
||||||
|
setInfoType('email');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setTouched(true);
|
||||||
|
validateInput(forgettedPasswordInfo, infoType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInput = (
|
||||||
|
value: string,
|
||||||
|
authType: AuthType,
|
||||||
|
setErrors: boolean = true,
|
||||||
|
) => {
|
||||||
|
if (!value) {
|
||||||
|
if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
|
||||||
|
return false;
|
||||||
|
} else if (authType === 'email' && !isEmail(value)) {
|
||||||
|
if (setErrors) setError(t('loginForm.emailIsInvalid'));
|
||||||
|
return false;
|
||||||
|
} else if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
|
||||||
|
if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (setErrors) setError(undefined);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (validateInput(forgettedPasswordInfo, infoType, false)) {
|
||||||
|
setSendCodeLoading(true);
|
||||||
|
|
||||||
|
const sendCodeRequest: SendForgetPassCodeRequest = {
|
||||||
|
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||||
|
phoneNumber:
|
||||||
|
infoType === 'phone'
|
||||||
|
? countryCode + forgettedPasswordInfo
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
const result = await sendForgetPassCode(sendCodeRequest);
|
||||||
|
const jsonRes = await result.json();
|
||||||
|
|
||||||
|
if (!jsonRes.success) {
|
||||||
|
setErrorMessage(jsonRes.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSendCodeLoading(false);
|
||||||
|
onVerifyOtp(forgettedPasswordInfo);
|
||||||
|
} else {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
validateInput(forgettedPasswordInfo, infoType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAdornment =
|
||||||
|
infoType === 'phone' && forgettedPasswordInfo.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationCard>
|
||||||
|
<Toast
|
||||||
|
color="error"
|
||||||
|
onClose={() => setErrorMessage(undefined)}
|
||||||
|
open={!!errorMessage}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</Toast>
|
||||||
|
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="h5">
|
||||||
|
{t('forgetPassword.forgetPassword')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t(
|
||||||
|
'forgetPassword.pleaseEnterYourMobileNumberEmailToRecoverYourPassword',
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
ref={textFieldRef}
|
||||||
|
inputRef={inputRef}
|
||||||
|
label={t('loginForm.emailOrPhoneLabel')}
|
||||||
|
value={forgettedPasswordInfo}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
error={inputError}
|
||||||
|
helperText={inputError ? error : ''}
|
||||||
|
autoFocus
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<CountryCodeSelector
|
||||||
|
value={countryCode}
|
||||||
|
onChange={setCountryCode}
|
||||||
|
show={showAdornment}
|
||||||
|
menuAnchor={textFieldRef.current}
|
||||||
|
onCloseFocusRef={inputRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{ my: 4, mb: 8 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Button loading={sendCodeLoading} onClick={handleSubmit}>
|
||||||
|
{t('forgetPassword.confirm')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</AuthenticationCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/features/authorization/index.ts
Normal file
0
src/features/authorization/index.ts
Normal file
20
src/features/authorization/routes/AuthenticationPage.tsx
Normal file
20
src/features/authorization/routes/AuthenticationPage.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { FlexBox } from '@/components/components/common/FlexBox';
|
||||||
|
import Logo from '@/components/Logo';
|
||||||
|
import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps';
|
||||||
|
|
||||||
|
export function AuthenticationPage() {
|
||||||
|
return (
|
||||||
|
<FlexBox
|
||||||
|
direction="column"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo />
|
||||||
|
<AuthenticationSteps />
|
||||||
|
</FlexBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
src/features/authorization/routes/ForgetPasswordPage.tsx
Normal file
20
src/features/authorization/routes/ForgetPasswordPage.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { FlexBox } from '@/components/components/common/FlexBox';
|
||||||
|
import Logo from '@/components/Logo';
|
||||||
|
import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer';
|
||||||
|
|
||||||
|
export function ForgetPasswordPage() {
|
||||||
|
return (
|
||||||
|
<FlexBox
|
||||||
|
direction="column"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo />
|
||||||
|
<ForgetPasswordContainer />
|
||||||
|
</FlexBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/features/authorization/types/authTypes.ts
Normal file
10
src/features/authorization/types/authTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type AuthType = 'email' | 'phone';
|
||||||
|
|
||||||
|
export type AuthMode = 'register' | 'login';
|
||||||
|
|
||||||
|
export type AuthStep =
|
||||||
|
| 'emailOrPhone'
|
||||||
|
| 'verify'
|
||||||
|
| 'enterPassword'
|
||||||
|
| 'addPhoneNumber'
|
||||||
|
| 'addedPhoneNumberVerify';
|
||||||
138
src/features/authorization/types/userTypes.ts
Normal file
138
src/features/authorization/types/userTypes.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
// GetUserStatusByPhoneNumberOrEmail
|
||||||
|
|
||||||
|
import type { ApiResponse } from '@/types/apiResponse';
|
||||||
|
import type { GUID } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
export interface GetUserStatusByPhoneNumberOrEmailRequest {
|
||||||
|
phoneNumber?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse {
|
||||||
|
userStatus: UserStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserStatus {
|
||||||
|
None = 0,
|
||||||
|
RegisteredWithPassword = 1,
|
||||||
|
RegisteredWithoutPassword = 2,
|
||||||
|
NotRegistered = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginOrSignUpWithOtp
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
otpCode: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
email?: string;
|
||||||
|
returnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordLoginRequest {
|
||||||
|
phoneNumber?: string;
|
||||||
|
email?: string;
|
||||||
|
password: string;
|
||||||
|
returnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse extends ApiResponse {
|
||||||
|
returnUrl: string;
|
||||||
|
userId: GUID;
|
||||||
|
registeredWithOutPhoneNumber: boolean;
|
||||||
|
completedUserInformation: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSmsOtp
|
||||||
|
|
||||||
|
export interface SendSmsOtpRequest {
|
||||||
|
phoneNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEmailOtp
|
||||||
|
|
||||||
|
export interface SendEmailOtpRequest {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmOtp
|
||||||
|
|
||||||
|
export interface ConfirmEmailOtpRequest {
|
||||||
|
email: string;
|
||||||
|
otpCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmSmsOtpRequest {
|
||||||
|
phoneNumber: string;
|
||||||
|
otpCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmOtpResponse extends ApiResponse {
|
||||||
|
confirm: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword
|
||||||
|
|
||||||
|
export interface ResetPasswordRequest {
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmNewPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPasswordResponse extends ApiResponse {
|
||||||
|
passwordChanged: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendForgetPassCode
|
||||||
|
|
||||||
|
export interface SendForgetPassCodeRequest {
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmForgetPassCode
|
||||||
|
|
||||||
|
export interface ConfirmForgetPassCodeRequest {
|
||||||
|
email?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginOrSignUpWithGoogle
|
||||||
|
|
||||||
|
export interface GoogleCodeClientResponse {
|
||||||
|
id_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginOrSignUpWithGoogleRequest {
|
||||||
|
idToken: string;
|
||||||
|
returnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginOrSignUpWithGoogleResponse extends ApiResponse {
|
||||||
|
userId: GUID;
|
||||||
|
registeredWithOutPhoneNumber: boolean;
|
||||||
|
completedUserInformation: boolean;
|
||||||
|
returnUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteUserInformation
|
||||||
|
|
||||||
|
export interface CompleteUserInformationRequest {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
gender?: Gender;
|
||||||
|
nationalCode?: string;
|
||||||
|
savePassword?: boolean;
|
||||||
|
password?: string;
|
||||||
|
saveEmail?: boolean;
|
||||||
|
email?: string;
|
||||||
|
birthDate?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
userId?: GUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Gender {
|
||||||
|
Male = 1,
|
||||||
|
Female = 2,
|
||||||
|
}
|
||||||
12
src/global.d.ts
vendored
Normal file
12
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace JSX {
|
||||||
|
interface Element extends React.ReactElement<any, any> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
google: typeof google;
|
||||||
|
}
|
||||||
|
const google: any;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { type ApiResponse } from '@/types/apiResponse';
|
import { type ApiResponse } from '@/types/apiResponse';
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ const apiClient = axios.create({
|
|||||||
|
|
||||||
// Set default headers
|
// Set default headers
|
||||||
headers: {
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
Authorization: `Bearer ${localStorage.getItem('authToken')}`,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,28 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
cssVariables: {
|
cssVariables: {
|
||||||
colorSchemeSelector: 'class',
|
colorSchemeSelector: 'class',
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
MuiTextField: {
|
||||||
|
defaultProps: {
|
||||||
|
variant: 'outlined',
|
||||||
|
fullWidth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiButton: {
|
||||||
|
defaultProps: {
|
||||||
|
size: 'large',
|
||||||
|
fullWidth: true,
|
||||||
|
variant: 'contained',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
typography: typography,
|
typography: typography,
|
||||||
});
|
});
|
||||||
}, [i18n]);
|
}, [i18n]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme} defaultMode="system">
|
<ThemeProvider theme={theme} defaultMode="light">
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import createCache from '@emotion/cache';
|
import createCache from '@emotion/cache';
|
||||||
import rtlPlugin from 'stylis-plugin-rtl';
|
import rtlPlugin from 'stylis-plugin-rtl';
|
||||||
|
import { prefixer } from 'stylis';
|
||||||
|
|
||||||
// This provider configures Emotion's cache to support RTL.
|
// This provider configures Emotion's cache to support RTL.
|
||||||
export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [cache, setCache] = useState(createCache({ key: 'css' }));
|
|
||||||
|
|
||||||
useEffect(() => {
|
const cacheRtl = useMemo(() => {
|
||||||
const newDir = i18n.dir(i18n.language);
|
const isRtl = i18n.dir(i18n.language) === 'rtl';
|
||||||
|
return createCache({
|
||||||
const newCache = createCache({
|
key: isRtl ? 'muirtl' : 'muiltr',
|
||||||
key: 'css',
|
stylisPlugins: isRtl ? [prefixer, rtlPlugin] : [],
|
||||||
stylisPlugins: newDir === 'rtl' ? [rtlPlugin] : [],
|
|
||||||
});
|
});
|
||||||
setCache(newCache);
|
}, [i18n]);
|
||||||
}, [i18n, i18n.language]);
|
|
||||||
|
|
||||||
return <CacheProvider value={cache}>{children}</CacheProvider>;
|
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
|
||||||
};
|
};
|
||||||
|
|||||||
119
src/routes/config.tsx
Normal file
119
src/routes/config.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { Layout } from '@/components/Layout/Layout';
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Devices,
|
||||||
|
LocationTick,
|
||||||
|
Mobile,
|
||||||
|
PasswordCheck,
|
||||||
|
Personalcard,
|
||||||
|
ProfileCircle,
|
||||||
|
Setting,
|
||||||
|
Shield,
|
||||||
|
Sms,
|
||||||
|
type Icon,
|
||||||
|
} from 'iconsax-react';
|
||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface RouteConfig {
|
||||||
|
path: string;
|
||||||
|
element?: ReactNode;
|
||||||
|
navConfig?: {
|
||||||
|
title: string; // Translation key
|
||||||
|
icon?: Icon;
|
||||||
|
};
|
||||||
|
children?: RouteConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));)
|
||||||
|
export const appRoutes: RouteConfig[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Navigate to="/setting/profile" replace />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting',
|
||||||
|
element: <Layout />,
|
||||||
|
children: [
|
||||||
|
// TODO: add route component to each route
|
||||||
|
{
|
||||||
|
path: '/setting/profile',
|
||||||
|
navConfig: {
|
||||||
|
// Profile component
|
||||||
|
title: 'side.account',
|
||||||
|
icon: ProfileCircle,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/setting/profile#info',
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.personalInfo',
|
||||||
|
icon: Personalcard,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/profile#contact-info',
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.contactInfo',
|
||||||
|
icon: Mobile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/profile#email',
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.email',
|
||||||
|
icon: Sms,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/security',
|
||||||
|
// security component
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.security',
|
||||||
|
icon: Shield,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/setting/security#password',
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.password',
|
||||||
|
icon: PasswordCheck,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/security#confirmed-ips',
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.confirmedIps',
|
||||||
|
icon: LocationTick,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/security#recent-sessions',
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.recentSessions',
|
||||||
|
icon: Devices,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/active-sessions',
|
||||||
|
// active session component
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.activeSessions',
|
||||||
|
icon: Calendar,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/setting/preferences',
|
||||||
|
// setting component
|
||||||
|
navConfig: {
|
||||||
|
title: 'side.setting',
|
||||||
|
icon: Setting,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
34
src/routes/index.tsx
Normal file
34
src/routes/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Suspense, type ReactNode } from 'react';
|
||||||
|
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
|
||||||
|
import { appRoutes, type RouteConfig } from './config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A recursive function to map our custom route config to the format
|
||||||
|
* that react-router-dom expects, applying layouts and guards.
|
||||||
|
*/
|
||||||
|
function mapRoutes(routes: RouteConfig[]): RouteObject[] {
|
||||||
|
return routes.map((route) => {
|
||||||
|
// Start with the base element, wrapped in Suspense for lazy loading
|
||||||
|
let element: ReactNode = (
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>{route.element}</Suspense>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Conditionally wrap the element in the specified layout
|
||||||
|
// if (route.layout) {
|
||||||
|
// element = <route.layout>{element}</route.layout>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Conditionally wrap the element in the authentication guard
|
||||||
|
// if (route.authRequired) {
|
||||||
|
// element = <ProtectedRoute>{element}</ProtectedRoute>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: route.path,
|
||||||
|
element: element,
|
||||||
|
...(route.children && { children: mapRoutes(route.children) }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const router = createBrowserRouter(mapRoutes(appRoutes));
|
||||||
@@ -49,9 +49,9 @@ export const PALETTE: Palette = {
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
light: {
|
light: {
|
||||||
main: '#E53935',
|
main: '#d32f2f',
|
||||||
dark: '#C62828',
|
dark: '#c62828',
|
||||||
light: '#EF5350',
|
light: '#ef5350',
|
||||||
contrastText: '#FFFFFF',
|
contrastText: '#FFFFFF',
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
@@ -91,9 +91,9 @@ export const PALETTE: Palette = {
|
|||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
light: {
|
light: {
|
||||||
main: '#43A047',
|
main: '#2e7d32',
|
||||||
dark: '#1B5E20',
|
dark: '#1b5e20',
|
||||||
light: '#81C784',
|
light: '#4caf50',
|
||||||
contrastText: '#FFFFFF',
|
contrastText: '#FFFFFF',
|
||||||
},
|
},
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
3
src/types/commonTypes.ts
Normal file
3
src/types/commonTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type GUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||||
|
|
||||||
|
export type CountryCode = `+${number}`;
|
||||||
5
src/types/fetchPromise.ts
Normal file
5
src/types/fetchPromise.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type FetchPromise<T = ''> = Promise<FetchResponse<T>>;
|
||||||
|
|
||||||
|
export interface FetchResponse<T> extends Response {
|
||||||
|
json(): Promise<T>;
|
||||||
|
}
|
||||||
1
src/utils/regexes/containsNumber.tsx
Normal file
1
src/utils/regexes/containsNumber.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const containsNumber = (value: string) => /\d/.test(value);
|
||||||
1
src/utils/regexes/containsSymbol.tsx
Normal file
1
src/utils/regexes/containsSymbol.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const containsSymbol = (value: string) => /[!@#$%&*\^]/.test(value);
|
||||||
5
src/utils/regexes/hasUpperAndLowerLetter.tsx
Normal file
5
src/utils/regexes/hasUpperAndLowerLetter.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const hasUpperAndLowerLetter = (value: string) => {
|
||||||
|
const hasUpper = /[A-Z]/.test(value);
|
||||||
|
const hasLower = /[a-z]/.test(value);
|
||||||
|
return hasUpper && hasLower;
|
||||||
|
};
|
||||||
2
src/utils/regexes/isEmail.tsx
Normal file
2
src/utils/regexes/isEmail.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const isEmail = (value: string) =>
|
||||||
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
|
||||||
1
src/utils/regexes/isNumeric.ts
Normal file
1
src/utils/regexes/isNumeric.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const isNumeric = (value: string) => /^\d*$/.test(value);
|
||||||
7
src/utils/regexes/isValidPhoneNumber.tsx
Normal file
7
src/utils/regexes/isValidPhoneNumber.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||||
|
|
||||||
|
export const isPhoneNumber = (code: string, phone: string) => {
|
||||||
|
const phoneNumber = parsePhoneNumberFromString(code + phone);
|
||||||
|
|
||||||
|
return phoneNumber && phoneNumber.isValid();
|
||||||
|
};
|
||||||
1
src/utils/regexes/least8Chars.tsx
Normal file
1
src/utils/regexes/least8Chars.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const least8Chars = (value: string) => value.length >= 8;
|
||||||
2
vite.config.d.ts
vendored
Normal file
2
vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import('vite').UserConfig;
|
||||||
|
export default _default;
|
||||||
12
vite.config.js
Normal file
12
vite.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user