diff --git a/.env b/.env
new file mode 100644
index 0000000..2731127
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 45b640a..6e974af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# npm registry
.npmrc
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 7c90784..cc58a0d 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -4,18 +4,18 @@
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
-- develop
+ - develop
pool:
vmImage: ubuntu-latest
steps:
-- task: NodeTool@0
- inputs:
- versionSpec: '20.x'
- displayName: 'Install Node.js'
+ - task: NodeTool@0
+ inputs:
+ versionSpec: '20.x'
+ displayName: 'Install Node.js'
-- script: |
- npm install
- npm run build
- displayName: 'npm install and build'
+ - script: |
+ npm install
+ npm run build
+ displayName: 'npm install and build'
diff --git a/index.html b/index.html
index 6e5d786..6255f17 100644
--- a/index.html
+++ b/index.html
@@ -22,6 +22,7 @@
})();
+
diff --git a/package-lock.json b/package-lock.json
index e29389d..62ba273 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,14 +8,20 @@
"name": "harmony-club",
"version": "0.0.0",
"dependencies": {
+ "@date-io/dayjs": "^3.2.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.3.1",
"@mui/stylis-plugin-rtl": "^7.2.0",
- "@mui/x-data-grid": "^8.10.0",
"@mui/x-virtualizer": "^0.1.1",
- "@rkheftan/harmony-ui": "^0.1.6",
+ "@mui/x-date-pickers": "^8.9.0",
+ "@mui/x-date-pickers-pro": "^8.9.0",
"axios": "^1.11.0",
+ "date-fns": "^4.1.0",
+ "date-fns-jalali": "^4.0.0-0",
+ "dayjs": "^1.11.13",
+ "@rkheftan/harmony-ui": "^0.1.6",
+ "@types/stylis": "^4.2.7",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
@@ -25,7 +31,7 @@
"react-country-flag": "^3.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0",
- "react-virtuoso": "^4.13.0",
+ "react-router-dom": "^7.8.0",
"stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1"
},
@@ -89,22 +95,22 @@
}
},
"node_modules/@babel/core": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
- "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
+ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.0",
+ "@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
- "@babel/helper-module-transforms": "^7.27.3",
- "@babel/helpers": "^7.27.6",
- "@babel/parser": "^7.28.0",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.3",
+ "@babel/parser": "^7.28.3",
"@babel/template": "^7.27.2",
- "@babel/traverse": "^7.28.0",
- "@babel/types": "^7.28.0",
+ "@babel/traverse": "^7.28.3",
+ "@babel/types": "^7.28.2",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -137,13 +143,13 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
- "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.0",
- "@babel/types": "^7.28.0",
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -202,15 +208,15 @@
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.27.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
- "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
- "@babel/traverse": "^7.27.3"
+ "@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -258,9 +264,9 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.28.2",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
- "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
+ "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -272,12 +278,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
- "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
+ "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.0"
+ "@babel/types": "^7.28.2"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -319,9 +325,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.28.2",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
- "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
+ "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -342,17 +348,17 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
- "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
+ "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.0",
+ "@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.0",
+ "@babel/parser": "^7.28.3",
"@babel/template": "^7.27.2",
- "@babel/types": "^7.28.0",
+ "@babel/types": "^7.28.2",
"debug": "^4.3.1"
},
"engines": {
@@ -372,6 +378,29 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@date-io/core": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@date-io/core/-/core-3.2.0.tgz",
+ "integrity": "sha512-hqwXvY8/YBsT9RwQITG868ZNb1MVFFkF7W1Ecv4P472j/ZWa7EFcgSmxy8PUElNVZfvhdvfv+a8j6NWJqOX5mA==",
+ "license": "MIT"
+ },
+ "node_modules/@date-io/dayjs": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-3.2.0.tgz",
+ "integrity": "sha512-+3LV+3N+cpQbEtmrFo8odg07k02AFY7diHgbi2EKYYANOOCPkDYUjDr2ENiHuYNidTs3tZwzDKckZoVNN4NXxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@date-io/core": "^3.2.0"
+ },
+ "peerDependencies": {
+ "dayjs": "^1.8.17"
+ },
+ "peerDependenciesMeta": {
+ "dayjs": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@@ -531,9 +560,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
- "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
"cpu": [
"ppc64"
],
@@ -548,9 +577,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
- "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
"cpu": [
"arm"
],
@@ -565,9 +594,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
- "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
"cpu": [
"arm64"
],
@@ -582,9 +611,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
- "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
"cpu": [
"x64"
],
@@ -599,9 +628,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
- "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
"cpu": [
"arm64"
],
@@ -616,9 +645,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
- "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
"cpu": [
"x64"
],
@@ -633,9 +662,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
- "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
"cpu": [
"arm64"
],
@@ -650,9 +679,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
- "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
"cpu": [
"x64"
],
@@ -667,9 +696,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
- "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
"cpu": [
"arm"
],
@@ -684,9 +713,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
- "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
"cpu": [
"arm64"
],
@@ -701,9 +730,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
- "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
"cpu": [
"ia32"
],
@@ -718,9 +747,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
- "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
"cpu": [
"loong64"
],
@@ -735,9 +764,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
- "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
"cpu": [
"mips64el"
],
@@ -752,9 +781,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
- "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
"cpu": [
"ppc64"
],
@@ -769,9 +798,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
- "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
"cpu": [
"riscv64"
],
@@ -786,9 +815,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
- "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
"cpu": [
"s390x"
],
@@ -803,9 +832,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
- "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
"cpu": [
"x64"
],
@@ -820,9 +849,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
- "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
"cpu": [
"arm64"
],
@@ -837,9 +866,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
- "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
"cpu": [
"x64"
],
@@ -854,9 +883,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
- "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
"cpu": [
"arm64"
],
@@ -871,9 +900,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
- "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
"cpu": [
"x64"
],
@@ -888,9 +917,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
- "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
"cpu": [
"arm64"
],
@@ -905,9 +934,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
- "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
"cpu": [
"x64"
],
@@ -922,9 +951,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
- "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
"cpu": [
"arm64"
],
@@ -939,9 +968,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
- "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
"cpu": [
"ia32"
],
@@ -956,9 +985,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
- "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
"cpu": [
"x64"
],
@@ -1245,7 +1274,7 @@
"resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-3.4.2.tgz",
"integrity": "sha512-3Ncze6JsJpB7BpYhqIgvBpfvEX1jsEKrad5hQBpyRQxtoAp6hx3+R46zqfsuQG4D9egQZ+xftQ0u4LPFMB7Wmg==",
"license": "MIT",
- "peer": true,
+ "peer": true,
"dependencies": {
"tslib": "^2.4.1"
}
@@ -1317,9 +1346,9 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.12",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
- "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1336,15 +1365,15 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.4",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
- "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.29",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
- "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1583,6 +1612,7 @@
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.10.0.tgz",
"integrity": "sha512-NMOZyDcE9vqn0qEv0z6DqkXwzIOj4ZFy4QC0RcUjEvBmjwdRc3KCh9XSWAuqmpc23B4M9cydVVkt0CBfOJKwsQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.28.2",
"@mui/utils": "^7.2.0",
@@ -1690,6 +1720,316 @@
}
}
},
+ "node_modules/@mui/x-date-pickers": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.10.0.tgz",
+ "integrity": "sha512-3nY+SS2/JtqcptQodECIyWKsTvPBDAcXKkyW65R4rQUCrnV6tuzriSrzy/FEYqTK0hyXYPIGJhQ6A0FbtQ9AkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.2",
+ "@mui/utils": "^7.2.0",
+ "@mui/x-internals": "8.10.0",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-date-pickers-pro": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-date-pickers-pro/-/x-date-pickers-pro-8.10.0.tgz",
+ "integrity": "sha512-ha678mLaYv/NikCRkcis0gNHhn/9IhODhEAXLqBqUY1o9kSd5v5cD+wKSo3T94sNNZPGdBhy87IB7m6g1EKLgA==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@babel/runtime": "^7.28.2",
+ "@mui/utils": "^7.2.0",
+ "@mui/x-date-pickers": "8.10.0",
+ "@mui/x-internals": "8.10.0",
+ "@mui/x-license": "8.10.0",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-internals": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.10.0.tgz",
+ "integrity": "sha512-stYhWBeCKfV2/ltAWShZ3ZJ51otbqpMpC+krWWoIsxM8TuvGzwXw5YMU9L2fTb8hRstsiOCQfEzIn12Ii7+N0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.2",
+ "@mui/utils": "^7.2.0",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@mui/x-license": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-license/-/x-license-8.10.0.tgz",
+ "integrity": "sha512-N6grkf44ESMmQp8bqSNKmWLIsf7IfsfJUr2PKDH07PVfJHTwvyQpUKBF+mLUtcd/GNDyUhqyDK98zx9AwGzSwA==",
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@babel/runtime": "^7.28.2",
+ "@mui/utils": "^7.2.0",
+ "@mui/x-internals": "8.10.0",
+ "@mui/x-telemetry": "8.5.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@mui/x-telemetry": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/@mui/x-telemetry/-/x-telemetry-8.5.3.tgz",
+ "integrity": "sha512-vBLVBXCBWY44HonjRefpYjowEXa25k2AtAXkWk2tHfL3/unnnexrYPosuo/p4giIWer0pMy/bPqGY2qM0xlM+g==",
+ "hasInstallScript": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "@babel/runtime": "^7.27.6",
+ "@fingerprintjs/fingerprintjs": "^3.4.2",
+ "ci-info": "^4.2.0",
+ "conf": "^11.0.2",
+ "is-docker": "^3.0.0",
+ "node-machine-id": "^1.1.12"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@mui/x-virtualizer": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.1.1.tgz",
+ "integrity": "sha512-pZ84wPu/97Z6g2HF7D4t8X5GSgc+Gr3EoJJpGv1SP3mAX2OcZtYhXiUyQzvHPm2jvDQuxIIzwXT3hMIEgdDPPQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.27.4",
+ "@mui/utils": "^7.2.0",
+ "@mui/x-internals": "8.10.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
+ "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-data-grid-pro": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/@mui/x-data-grid-pro/-/x-data-grid-pro-8.10.0.tgz",
+ "integrity": "sha512-jg5WZakq8QVnYgF1KQ6EFWqtjPXl5Aww4o9bJQOiq1I5IGXqQJdVm9VGdDK0Xywn+FdNiU4VbdQhS++B601b5w==",
+ "license": "SEE LICENSE IN LICENSE",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.28.2",
+ "@mui/utils": "^7.2.0",
+ "@mui/x-data-grid": "8.10.0",
+ "@mui/x-internals": "8.10.0",
+ "@mui/x-license": "8.10.0",
+ "@types/format-util": "^1.0.4",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@mui/x-internals": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.10.0.tgz",
@@ -2202,9 +2542,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.2.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
- "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
+ "version": "24.3.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
+ "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2259,17 +2599,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz",
- "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
+ "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.39.0",
- "@typescript-eslint/type-utils": "8.39.0",
- "@typescript-eslint/utils": "8.39.0",
- "@typescript-eslint/visitor-keys": "8.39.0",
+ "@typescript-eslint/scope-manager": "8.39.1",
+ "@typescript-eslint/type-utils": "8.39.1",
+ "@typescript-eslint/utils": "8.39.1",
+ "@typescript-eslint/visitor-keys": "8.39.1",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -2283,22 +2623,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.39.0",
+ "@typescript-eslint/parser": "^8.39.1",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz",
- "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
+ "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.39.0",
- "@typescript-eslint/types": "8.39.0",
- "@typescript-eslint/typescript-estree": "8.39.0",
- "@typescript-eslint/visitor-keys": "8.39.0",
+ "@typescript-eslint/scope-manager": "8.39.1",
+ "@typescript-eslint/types": "8.39.1",
+ "@typescript-eslint/typescript-estree": "8.39.1",
+ "@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4"
},
"engines": {
@@ -2314,14 +2654,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
- "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
+ "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.39.0",
- "@typescript-eslint/types": "^8.39.0",
+ "@typescript-eslint/tsconfig-utils": "^8.39.1",
+ "@typescript-eslint/types": "^8.39.1",
"debug": "^4.3.4"
},
"engines": {
@@ -2336,14 +2676,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
- "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
+ "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.39.0",
- "@typescript-eslint/visitor-keys": "8.39.0"
+ "@typescript-eslint/types": "8.39.1",
+ "@typescript-eslint/visitor-keys": "8.39.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2354,9 +2694,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
- "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
+ "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2371,15 +2711,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz",
- "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
+ "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.39.0",
- "@typescript-eslint/typescript-estree": "8.39.0",
- "@typescript-eslint/utils": "8.39.0",
+ "@typescript-eslint/types": "8.39.1",
+ "@typescript-eslint/typescript-estree": "8.39.1",
+ "@typescript-eslint/utils": "8.39.1",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -2396,9 +2736,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
- "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
+ "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2410,16 +2750,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
- "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
+ "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.39.0",
- "@typescript-eslint/tsconfig-utils": "8.39.0",
- "@typescript-eslint/types": "8.39.0",
- "@typescript-eslint/visitor-keys": "8.39.0",
+ "@typescript-eslint/project-service": "8.39.1",
+ "@typescript-eslint/tsconfig-utils": "8.39.1",
+ "@typescript-eslint/types": "8.39.1",
+ "@typescript-eslint/visitor-keys": "8.39.1",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2439,16 +2779,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz",
- "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
+ "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.39.0",
- "@typescript-eslint/types": "8.39.0",
- "@typescript-eslint/typescript-estree": "8.39.0"
+ "@typescript-eslint/scope-manager": "8.39.1",
+ "@typescript-eslint/types": "8.39.1",
+ "@typescript-eslint/typescript-estree": "8.39.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2463,13 +2803,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
- "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
+ "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/types": "8.39.1",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -2493,6 +2833,19 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -2559,7 +2912,6 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ajv": "^8.0.0"
},
@@ -2682,6 +3034,168 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
+ "node_modules/atomically": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
+ "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==",
+ "dependencies": {
+ "stubborn-fs": "^1.2.5",
+ "when-exit": "^2.1.1"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^3.2.4",
+ "buffer-crc32": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "readdir-glob": "^1.1.2",
+ "tar-stream": "^2.2.0",
+ "zip-stream": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "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": "MIT",
+ "peer": true
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "license": "Unlicense",
+ "peer": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+ "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/atomically": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz",
@@ -2920,9 +3434,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001734",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
- "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
+ "version": "1.0.30001735",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz",
+ "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==",
"dev": true,
"funding": [
{
@@ -2981,7 +3495,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -3054,7 +3567,6 @@
"resolved": "https://registry.npmjs.org/conf/-/conf-11.0.2.tgz",
"integrity": "sha512-jjyhlQ0ew/iwmtwsS2RaB6s8DBifcE2GYBEaw2SJDUY/slJJbNfY4GlDVzOs/ff8cM/Wua5CikqXgbFl5eu85A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
@@ -3083,7 +3595,6 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -3186,6 +3697,22 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/date-fns-jalali": {
+ "version": "4.0.0-0",
+ "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.0.0-0.tgz",
+ "integrity": "sha512-EczB+gWceuWCRlacE4T+WmdP+BV/IUQpjQW9aBa9DNcXkKuZFv3WBDqeP2Ew+6YFBtPRRcH5U22+C6gcpwgG8A==",
+ "license": "MIT"
+ },
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -3198,7 +3725,6 @@
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz",
"integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"mimic-fn": "^4.0.0"
},
@@ -3257,7 +3783,6 @@
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz",
"integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"type-fest": "^2.11.2"
},
@@ -3326,9 +3851,10 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.199",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz",
- "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==",
+
+ "version": "1.5.202",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.202.tgz",
+ "integrity": "sha512-NxbYjRmiHcHXV1Ws3fWUW+SLb62isauajk45LUJ/HgIOkUA7jLZu/X2Iif+X9FBNK8QkF9Zb4Q2mcwXCcY30mg==",
"dev": true,
"license": "ISC"
},
@@ -3347,7 +3873,6 @@
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
@@ -3410,9 +3935,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
- "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -3423,32 +3948,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.8",
- "@esbuild/android-arm": "0.25.8",
- "@esbuild/android-arm64": "0.25.8",
- "@esbuild/android-x64": "0.25.8",
- "@esbuild/darwin-arm64": "0.25.8",
- "@esbuild/darwin-x64": "0.25.8",
- "@esbuild/freebsd-arm64": "0.25.8",
- "@esbuild/freebsd-x64": "0.25.8",
- "@esbuild/linux-arm": "0.25.8",
- "@esbuild/linux-arm64": "0.25.8",
- "@esbuild/linux-ia32": "0.25.8",
- "@esbuild/linux-loong64": "0.25.8",
- "@esbuild/linux-mips64el": "0.25.8",
- "@esbuild/linux-ppc64": "0.25.8",
- "@esbuild/linux-riscv64": "0.25.8",
- "@esbuild/linux-s390x": "0.25.8",
- "@esbuild/linux-x64": "0.25.8",
- "@esbuild/netbsd-arm64": "0.25.8",
- "@esbuild/netbsd-x64": "0.25.8",
- "@esbuild/openbsd-arm64": "0.25.8",
- "@esbuild/openbsd-x64": "0.25.8",
- "@esbuild/openharmony-arm64": "0.25.8",
- "@esbuild/sunos-x64": "0.25.8",
- "@esbuild/win32-arm64": "0.25.8",
- "@esbuild/win32-ia32": "0.25.8",
- "@esbuild/win32-x64": "0.25.8"
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
}
},
"node_modules/escalade": {
@@ -3651,6 +4176,36 @@
"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": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3888,8 +4443,7 @@
"url": "https://opencollective.com/fastify"
}
],
- "license": "BSD-3-Clause",
- "peer": true
+ "license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.19.1",
@@ -4053,6 +4607,23 @@
"node": ">=0.6"
}
},
+ "node_modules/fstream": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ },
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4131,6 +4702,65 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -4281,9 +4911,9 @@
}
},
"node_modules/i18next": {
- "version": "25.3.4",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.4.tgz",
- "integrity": "sha512-AHklEYFLiRRxW1Cb6zE9lfnEtYvsydRC8nRS3RSKGX3zCqZ8nLZwMaUsrb80YuccPNv2RNokDL8LkTNnp+6mDw==",
+ "version": "25.3.6",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.6.tgz",
+ "integrity": "sha512-dThZ0CTCM3sUG/qS0ZtQYZQcUI6DtBN8yBHK+SKEqihPcEYmjVWh/YJ4luic73Iq6Uxhp6q7LJJntRK5+1t7jQ==",
"funding": [
{
"type": "individual",
@@ -4450,7 +5080,6 @@
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
"license": "MIT",
- "peer": true,
"bin": {
"is-docker": "cli.js"
},
@@ -4563,8 +5192,7 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz",
"integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==",
- "license": "BSD-2-Clause",
- "peer": true
+ "license": "BSD-2-Clause"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -4927,7 +5555,6 @@
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -5030,8 +5657,7 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz",
"integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.19",
@@ -5429,7 +6055,6 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -5452,7 +6077,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
"integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"react-router": "7.8.0"
},
@@ -5480,16 +6104,6 @@
"react-dom": ">=16.6.0"
}
},
- "node_modules/react-virtuoso": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.14.0.tgz",
- "integrity": "sha512-fR+eiCvirSNIRvvCD7ueJPRsacGQvUbjkwgWzBZXVq+yWypoH7mRUvWJzGHIdoRaCZCT+6mMMMwIG2S1BW3uwA==",
- "license": "MIT",
- "peerDependencies": {
- "react": ">=16 || >=17 || >= 18 || >= 19",
- "react-dom": ">=16 || >=17 || >= 18 || >=19"
- }
- },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -5533,7 +6147,6 @@
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5718,8 +6331,7 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
@@ -5796,8 +6408,7 @@
"node_modules/stubborn-fs": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz",
- "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==",
- "peer": true
+ "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="
},
"node_modules/stylis": {
"version": "4.3.6",
@@ -5893,11 +6504,14 @@
}
},
"node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
- "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -5997,7 +6611,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
- "peer": true,
"engines": {
"node": ">=12.20"
},
@@ -6020,16 +6633,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.39.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz",
- "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==",
+ "version": "8.39.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz",
+ "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.39.0",
- "@typescript-eslint/parser": "8.39.0",
- "@typescript-eslint/typescript-estree": "8.39.0",
- "@typescript-eslint/utils": "8.39.0"
+ "@typescript-eslint/eslint-plugin": "8.39.1",
+ "@typescript-eslint/parser": "8.39.1",
+ "@typescript-eslint/typescript-estree": "8.39.1",
+ "@typescript-eslint/utils": "8.39.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6170,9 +6783,9 @@
}
},
"node_modules/vite": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz",
- "integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz",
+ "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6245,11 +6858,14 @@
}
},
"node_modules/vite/node_modules/fdir": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
- "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -6301,8 +6917,7 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz",
"integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
diff --git a/package.json b/package.json
index 2591048..cd776c5 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "@date-io/dayjs": "^3.2.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.3.1",
@@ -18,7 +19,12 @@
"@mui/x-data-grid": "^8.10.0",
"@mui/x-virtualizer": "^0.1.1",
"@rkheftan/harmony-ui": "^0.1.6",
+ "@mui/x-date-pickers": "^8.9.0",
+ "@mui/x-date-pickers-pro": "^8.9.0",
"axios": "^1.11.0",
+ "date-fns": "^4.1.0",
+ "date-fns-jalali": "^4.0.0-0",
+ "dayjs": "^1.11.13",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
@@ -28,7 +34,7 @@
"react-country-flag": "^3.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0",
- "react-virtuoso": "^4.13.0",
+ "react-router-dom": "^7.8.0",
"stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1"
},
diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json
new file mode 100644
index 0000000..6633ea7
--- /dev/null
+++ b/public/locales/en/authentication.json
@@ -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"
+ }
+}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index bc4b6ab..72b36d8 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -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"
+ }
}
diff --git a/public/locales/en/completionForm.json b/public/locales/en/completionForm.json
new file mode 100644
index 0000000..34572a5
--- /dev/null
+++ b/public/locales/en/completionForm.json
@@ -0,0 +1,38 @@
+{
+ "completion": {
+ "title": "Completion of user account information",
+ "description": "Enter your business information",
+ "name": "Name",
+ "familyName": "Family Name",
+ "gender": "Gender",
+ "optionalNationalCode": "National Code(Optional)",
+ "determinePassword": "Determine Password",
+ "password": "Password",
+ "passwordRepetition": "Repeat password",
+ "determineEmail": "Connect your email",
+ "email": "Email",
+ "vericationCodeButton": "Send verification code",
+ "verificationCode": "verification code",
+ "checkCodeButton": "Check code",
+ "registerButton": "Confirm and Register",
+ "man": "male",
+ "woman": "female",
+ "hasNumber": "includes number",
+ "hasMinLength": "at least 8 characters",
+ "hasUpperAndLower": "includes a lowercase and uppercase letter",
+ "hasSpecialChar": "includes sign (!@#$%^&*)",
+ "notCompatibility": "does not match",
+ "emailCorrectForm": "Enter the correct email form.",
+ "agreementPart1": "By continuing the registration process, you agree to the",
+ "agreementLinkText": " Harmony Terms and Conditions",
+ "agreementPart2": ".",
+ "sent": "sent",
+ "country": "country",
+ "dateOfBirth": "Date of birth(optional)",
+ "submitSuccess": "Information successfully registered",
+ "submitError": "Error in registering information",
+ "submitting": "Submitting...",
+ "success": "Success",
+ "agreement": "1. Confidentiality of Information: Harmony commits under no circumstances to disclose users’ identity information, such as phone numbers, email addresses, passwords, user IDs, or any related data, to third parties.\n\n2. User information is used solely for providing authentication services and remains confidential even after account deactivation or termination.\n\n3. Harmony is obliged to implement necessary security measures to prevent unauthorized access.\n\n4. Responsibility for Account Security: Users must protect their accounts and choose strong, non-guessable passwords. Periodic password changes and immediate action in case of suspected unauthorized access are required. Any misuse of the account due to user negligence is the responsibility of the user.\n\n5. Security Breaches and Cyber Attacks: Harmony is not responsible for security breaches caused by cyber attacks beyond the system's control. However, Harmony employs up-to-date security standards and encryption to prevent such incidents.\n\n6. User Negligence in Protecting Information: If account information is disclosed due to user negligence or error, Harmony bears no responsibility. Determining such cases, based on system security logs, is the responsibility of Harmony's technical manager.\n\n7. Accurate Logging of Activities: All events related to registering, editing, and deleting information in the system are accurately and immutably logged. Claims regarding deletion or modification of data without logs are invalid unless supported by documentation provided by the user.\n\n8. Service Updates: Harmony services may be updated or changed over time. Continued use of the system after changes implies acceptance of the new terms. If users disagree, they may request account deletion.\n\n9. User Support: Support is provided only via email and phone, free of charge. Harmony is not obligated to provide in-person support or training beyond basic services.\n\n10. Official Communication Channels: Harmony communicates with users only via the phone number and email registered in the user account. Official announcements are sent through these channels.\n\n11. Official Domains for Communication: All emails from Harmony are sent exclusively from the domain harmony.id. Users must verify this to prevent phishing or similar attacks.\n\n12. Compliance with Iranian Laws: Users must comply with all applicable laws of the Islamic Republic of Iran, including the “Electronic Commerce Law,” “Computer Crimes Law,” and related legislation. Responsibility for violations rests with the user.\n\n13. Temporary Data Retention After Account Termination: Upon account termination or deletion, user information is stored securely for 30 days and permanently deleted thereafter.\n\n14. Ownership of User Data: All data submitted by users belongs to them. Harmony has no ownership over this information. Users are responsible for the accuracy, quality, and legality of their data.\n\n15. Purposeful Use of Identity Information: Collected identity information during registration is used only for authentication and basic services. It will not be shared with any third party without explicit user consent, except under a court order or legal authority.\n\n16. Permanent Data Confidentiality: Harmony commits to maintaining confidentiality of collected information even after the end of the user relationship or account closure.\n\n17. Limitation of Liability: Harmony is not liable for direct or indirect damages resulting from use or inability to use the authentication services.\n\n18. Disruptions in Communication Infrastructure: Harmony is not responsible for disruptions caused by the internet, infrastructure services, or other issues beyond its control.\n\n19. Force Majeure and Unforeseen Events: Harmony bears no responsibility for natural disasters, strikes, power outages, cyber attacks, or other events beyond its control that prevent service delivery.\n\n20. Services Dependent on Third Parties: If parts of the authentication services are provided by third parties, the usage terms of those services are the responsibility of those companies, and Harmony bears no liability.\n\n21. Guarantee of Data Access in Case of Service Termination: If Harmony ceases operations permanently, it commits to keeping servers active for two years and allowing users access to their data.\n\n22. Notification of Service Interruptions: If service interruption is necessary, Harmony must notify users at least 12 hours in advance via email or SMS."
+ }
+}
diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json
new file mode 100644
index 0000000..bfb8610
--- /dev/null
+++ b/public/locales/fa/authentication.json
@@ -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": "رمز عبور با موفقیت تغییر یافت"
+ }
+}
diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json
index 3f4cd0d..60424fb 100644
--- a/public/locales/fa/common.json
+++ b/public/locales/fa/common.json
@@ -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": "تنظیمات"
+ }
}
diff --git a/public/locales/fa/completionForm.json b/public/locales/fa/completionForm.json
new file mode 100644
index 0000000..273ea69
--- /dev/null
+++ b/public/locales/fa/completionForm.json
@@ -0,0 +1,46 @@
+{
+ "completion": {
+ "title": "تکمیل اطلاعات حساب کاربری",
+ "description": "اطلاعات کسب و کار خود را وارد کنید",
+ "name": "نام",
+ "familyName": "نام خانوادگی",
+ "gender": "جنسیت",
+ "optionalNationalCode": "کدملی(اختیاری)",
+ "determinePassword": "تعیین رمز عبور",
+ "password": "رمز عبور",
+ "passwordRepetition": "تکرار رمز عبور",
+ "determineEmail": "اتصال ایمیل خود",
+ "email": "ایمیل",
+ "vericationCodeButton": "ارسال کد تایید",
+ "verificationCode": "کد تایید",
+ "checkCodeButton": "بررسی کد",
+ "registerButton": "تایید و ثبت نام",
+ "man": "مرد",
+ "woman": "زن",
+ "hasNumber": "شامل عدد",
+ "hasMinLength": "حداقل 8 کاراکتر",
+ "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ",
+ "hasSpecialChar": "شامل علامت (!@#$%^&*)",
+ "notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد",
+ "emailCorrectForm": "ساختار ایمیل صحیح نیست",
+ "agreementPart1": " ادامه فرایند ثبت نام به منزله تایید و قبول",
+ "agreementLinkText": " قوانین و مقررات هارمونی",
+ "agreementPart2": "می باشد.",
+ "sent": "ارسال شد!",
+ "country": "کشور",
+ "dateOfBirth": "تاریخ تولد(اختیاری)",
+ "invalidCountry": "کشور انتخاب شده صحیح نیست",
+ "rules": "قوانین و مقررات",
+ "alertSuccess": "ایمیل با موفقیت تایید شد",
+ "submitSuccess": "اطلاعات با موفقیت ثبت شد",
+ "submitError": "خطا در ثبت اطلاعات",
+ "submitting": "در حال تایید...",
+ "success": "موفقیت",
+ "successfullCodeSent": "کد با موفقیت ارسال شد",
+ "codeSentBut": "کد ارسال شد اما",
+ "problem": "مشکلی پیش آمده",
+ "codeVerified": "کد با موفقیت تایید شد",
+ "invalidCode": "کد نامعتبر است",
+ "agreement": "۱. محرمانگی اطلاعات هارمونی متعهد میشود تحت هیچ شرایطی اطلاعات هویتی کاربران نظیر شماره تلفن، ایمیل، رمز عبور، شناسه کاربری و هرگونه داده مرتبط را در اختیار اشخاص ثالث قرار ندهد. اطلاعات کاربران صرفاً در چارچوب ارائه خدمات احراز هویت مورد استفاده قرار گرفته و حتی پس از غیرفعالسازی حساب یا قطع همکاری، این اطلاعات محرمانه باقی خواهد ماند. هارمونی موظف به پیادهسازی تدابیر امنیتی لازم برای جلوگیری از هرگونه دسترسی غیرمجاز میباشد.\n\n۲. مسئولیت حفظ اطلاعات ورود: کاربر موظف است از حساب کاربری خود محافظت کند و رمز عبوری ایمن و غیرقابل حدس انتخاب نماید. تغییر دورهای رمز عبور و اقدام فوری در صورت احساس خطر دسترسی غیرمجاز الزامی است. مسئولیت هرگونه سوءاستفاده از حساب کاربری به دلیل بیاحتیاطی کاربر، بر عهده خود وی خواهد بود.\n\n۳. رخنههای امنیتی و حملات سایبری: هارمونی در برابر رخنههای امنیتی ناشی از حملات سایبری که خارج از کنترل سیستم است، مسئولیتی ندارد. با این حال، هارمونی از بهروزترین استانداردهای امنیتی و رمزنگاری برای جلوگیری از چنین حوادثی بهره میبرد.\n\n۴. قصور کاربر در حفاظت از اطلاعات: چنانچه اطلاعات حساب کاربری به دلیل سهلانگاری یا اشتباه کاربر افشا شود، هارمونی مسئولیتی در این خصوص ندارد. تشخیص چنین مواردی بر اساس لاگهای امنیتی سیستم، بر عهده مدیر فنی هارمونی خواهد بود.\n\n۵. ثبت دقیق فعالیتها در لاگها: تمامی رویدادهای مرتبط با ثبت، ویرایش و حذف اطلاعات در سیستم، بهصورت دقیق و غیرقابلتغییر در لاگ کاربران ثبت میگردد. هرگونه ادعا مبنی بر حذف یا تغییر دادهها بدون ردپای لاگ قابل پذیرش نیست مگر با ارائه مستندات از سوی کاربر.\n\n۶. بهروزرسانی خدمات: خدمات هارمونی ممکن است به مرور زمان بهروزرسانی یا تغییر پیدا کند. ادامه استفاده از سیستم پس از اعمال تغییرات به معنای پذیرش مقررات جدید است. در صورت عدم موافقت، کاربر میتواند درخواست حذف حساب خود را ارائه دهد.\n\n۷. پشتیبانی کاربران: پشتیبانی خدمات صرفاً از طریق ایمیل و تماس تلفنی صورت میگیرد و رایگان است. هارمونی تعهدی به ارائه پشتیبانی حضوری یا آموزشهای فراتر از خدمات پایه ندارد.\n\n۸. راههای ارتباط رسمی: هارمونی تنها از طریق شماره تلفن و ایمیل ثبتشده در حساب کاربر با وی در ارتباط خواهد بود. اطلاعیهها و اعلانات رسمی از این مسیرها انجام میگیرد.\n\n۹. دامنههای رسمی ارتباط: تمام ایمیلهای ارسالی از سوی هارمونی صرفاً با دامنهی harmony.id ارسال میشود. کاربران موظف به بررسی این نشانی برای جلوگیری از فیشینگ و حملات مشابه هستند.\n\n۱۰. رعایت قوانین جمهوری اسلامی ایران: کاربر موظف است در استفاده از سیستم، کلیه قوانین جاری کشور، از جمله «قانون تجارت الکترونیکی»، «قانون جرائم رایانهای» و سایر قوانین مرتبط را رعایت نماید. مسئولیت هرگونه تخلف بر عهده کاربر خواهد بود.\n\n۱۱. نگهداری موقت اطلاعات پس از فسخ حساب: در صورت فسخ یا حذف حساب، اطلاعات کاربر به مدت ۳۰ روز در فضای امن نگهداری میشود و پس از آن، بهطور غیرقابلبازگشت حذف خواهد شد.\n\n۱۲. مالکیت اطلاعات کاربر: تمام اطلاعات ثبتشده توسط کاربر متعلق به خود اوست و هارمونی هیچگونه مالکیتی بر این اطلاعات ندارد. کاربر مسئول صحت، کیفیت و قانونی بودن دادههای خود میباشد.\n\n۱۳. استفاده هدفمند از اطلاعات شناسایی: اطلاعات هویتی جمعآوریشده هنگام ثبتنام تنها برای احراز هویت و ارائه خدمات پایه مورد استفاده قرار میگیرد. این اطلاعات بدون رضایت صریح کاربر، به هیچ نهاد یا شخص ثالثی منتقل نخواهد شد. تبصره: اطلاعات هویتی کاربران صرفاً در صورت حکم مقام قضایی یا مراجع ذیصلاح و در چارچوب قوانین، قابل ارائه خواهد بود.\n\n۱۴. محرمانگی دائمی دادهها: هارمونی متعهد است حتی پس از اتمام رابطه کاربری یا انحلال حساب، اطلاعات جمعآوریشده را به عنوان محرمانه حفظ نماید.\n\n۱۵. محدودیت مسئولیت: هارمونی مسئولیتی در قبال خسارات مستقیم یا غیرمستقیمی که به دلیل استفاده یا عدم استفاده از خدمات احراز هویت ایجاد شود، نخواهد داشت.\n\n۱۶. اختلال در بسترهای ارتباطی: هارمونی در برابر اختلالهای ناشی از شبکه اینترنت، خدمات زیرساختی یا هرگونه مشکل خارج از کنترل خود، مسئولیتی ندارد.\n\n۱۷. حوادث قهری و غیرمترقبه: در صورت وقوع بلایای طبیعی، اعتصاب، قطعی برق، حملات سایبری یا هرگونه رخداد خارج از کنترل هارمونی که مانع ارائه خدمات شود، مسئولیتی متوجه هارمونی نخواهد بود.\n\n۱۸. خدمات وابسته به سایر سرویسها: چنانچه بخشی از خدمات احراز هویت توسط شرکتهای ثالث ارائه شود، قوانین استفاده از این سرویسها بر عهده همان شرکتهاست و هارمونی نسبت به آنها مسئولیتی ندارد.\n\n۱۹. تضمین دسترسی به اطلاعات در صورت توقف فعالیت: در صورت توقف دائمی فعالیت هارمونی، این شرکت متعهد است به مدت دو سال، سرورها را فعال نگه دارد و امکان دسترسی کاربران به اطلاعات خود را فراهم سازد.\n\n۲۰. اطلاعرسانی در مورد قطع سرویسها: در صورت نیاز به توقف خدمات، هارمونی موظف است حداقل ۱۲ ساعت قبل، این موضوع را از طریق ایمیل یا پیامک به اطلاع کاربران برساند."
+ }
+}
diff --git a/src/App.tsx b/src/App.tsx
index 04360f5..ec7f7b6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,12 +1,15 @@
import { CssBaseline } from '@mui/material';
import './App.css';
-import { LanguageManager } from './components/LanguageManager';
+import { LanguageManager } from '@/components/LanguageManager';
+import { RouterProvider } from 'react-router-dom';
+import { router } from '@/routes';
function App() {
return (
<>
+
>
);
}
diff --git a/src/components/CountryFlag.tsx b/src/components/CountryFlag.tsx
index 3102050..ed1bbee 100644
--- a/src/components/CountryFlag.tsx
+++ b/src/components/CountryFlag.tsx
@@ -1,6 +1,7 @@
import { Box, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
-import { countries } from '@/features/profile/data/countries';
+// TODO: move countries outside of feature directory
+import { countries } from '@/features/authentication/data/Countries';
interface CountryFlagProps {
code: string;
diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx
new file mode 100644
index 0000000..39f4eb9
--- /dev/null
+++ b/src/components/Layout/Header.tsx
@@ -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 = ({ user }) => {
+ return (
+ t.spacing(10.5),
+ }}
+ >
+
+
+ {user.firstName + ' ' + user.lastName}
+
+
+ {user.phoneNumber}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..2652dfe
--- /dev/null
+++ b/src/components/Layout/Layout.tsx
@@ -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({
+ firstName: 'محمد حسین',
+ lastName: 'برزه گر',
+ phoneNumber: '09123456789',
+ });
+
+ return (
+
+
+
+
+
+
+
+ setSideNavOpen(false)}
+ header={isMobile ? undefined : }
+ footer={isMobile ? : undefined}
+ navConfig={navItemConfigs}
+ activePath={location.pathname + location.hash}
+ selectedVariant="textOnly"
+ positioning="absolute"
+ sideNavVariant={isMobile ? 'temporary' : 'full'}
+ top={8.125}
+ />
+
+
+ );
+};
diff --git a/src/components/Layout/Toolbar.tsx b/src/components/Layout/Toolbar.tsx
new file mode 100644
index 0000000..46b482e
--- /dev/null
+++ b/src/components/Layout/Toolbar.tsx
@@ -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>;
+ isMobile: boolean;
+ user: User;
+}
+
+export const Toolbar: React.FC = ({
+ sideNavOpen,
+ setSideNavOpen,
+ isMobile,
+ user,
+}) => {
+ return (
+ 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',
+ }}
+ >
+
+ {isMobile && (
+ setSideNavOpen(!sideNavOpen)}
+ >
+
+
+ )}
+ {/* */}
+ LOGO placeholder
+
+
+ {isMobile && (
+
+ {user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/Layout/buildNavItems.tsx b/src/components/Layout/buildNavItems.tsx
new file mode 100644
index 0000000..4cfaba0
--- /dev/null
+++ b/src/components/Layout/buildNavItems.tsx
@@ -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 ? (
+
+ ) : 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,
+ };
+ });
+}
diff --git a/src/components/Layout/type.ts b/src/components/Layout/type.ts
new file mode 100644
index 0000000..ab31407
--- /dev/null
+++ b/src/components/Layout/type.ts
@@ -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;
+}
diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx
new file mode 100644
index 0000000..d9376be
--- /dev/null
+++ b/src/components/Toast.tsx
@@ -0,0 +1,27 @@
+import { Alert, Snackbar, type AlertColor } from '@mui/material';
+import { type PropsWithChildren } from 'react';
+
+export interface ToastProps extends PropsWithChildren {
+ color: AlertColor | undefined;
+ open: boolean;
+ onClose: () => void;
+}
+
+export const Toast = ({ color, open, onClose, children }: ToastProps) => {
+ return (
+ `calc(100% - ${t.spacing(6)})`, maxWidth: '396px' }}
+ open={open}
+ onClose={onClose}
+ >
+
+ {children}
+
+
+ );
+};
diff --git a/src/components/components/DigitsInput.tsx b/src/components/components/DigitsInput.tsx
new file mode 100644
index 0000000..88932f3
--- /dev/null
+++ b/src/components/components/DigitsInput.tsx
@@ -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>;
+}
+
+const DigitInput: React.FC = ({
+ onChange,
+ error,
+ success,
+}) => {
+ const [code, setCode] = useState(['', '', '', '']);
+ const inputRefs = useRef>([]);
+
+ 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,
+ 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 (
+
+ {code.map((digit, index) => (
+ (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',
+ }}
+ />
+ ))}
+
+ );
+};
+
+export default DigitInput;
diff --git a/src/components/components/common/Container.tsx b/src/components/components/common/Container.tsx
new file mode 100644
index 0000000..c9efc78
--- /dev/null
+++ b/src/components/components/common/Container.tsx
@@ -0,0 +1,8 @@
+import { Box, styled } from '@mui/material';
+
+export const Container = styled(Box)(() => ({
+ width: '100%',
+ maxWidth: '100vw',
+ height: '100vh',
+ margin: '0 auto',
+}));
diff --git a/src/components/components/common/FlexBox.tsx b/src/components/components/common/FlexBox.tsx
new file mode 100644
index 0000000..e15bb8c
--- /dev/null
+++ b/src/components/components/common/FlexBox.tsx
@@ -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',
+})(
+ ({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({
+ display: 'flex',
+ flexDirection: direction,
+ justifyContent: justify,
+ alignItems: align,
+ }),
+);
diff --git a/src/components/components/common/Stack.tsx b/src/components/components/common/Stack.tsx
new file mode 100644
index 0000000..0e00bfd
--- /dev/null
+++ b/src/components/components/common/Stack.tsx
@@ -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',
+})(
+ ({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({
+ display: 'flex',
+ flexDirection: direction,
+ alignItems: align,
+ gap: theme.spacing(spacing),
+ }),
+);
diff --git a/src/countries.ts b/src/countries.ts
new file mode 100644
index 0000000..a2a0166
--- /dev/null
+++ b/src/countries.ts
@@ -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' },
+];
diff --git a/src/features/authentication/api/userCompletion.ts b/src/features/authentication/api/userCompletion.ts
new file mode 100644
index 0000000..d9fcf26
--- /dev/null
+++ b/src/features/authentication/api/userCompletion.ts
@@ -0,0 +1,32 @@
+import {
+ type SendEmailOtpPayload,
+ type ConfirmEmailOtpPayload,
+ type CompleteUserInfoPayload,
+ type CompleteUserInfoResponse,
+ type GenericApiResponse,
+} from '../types/completionFormApiTypes';
+import apiClient from '@/lib/apiClient';
+
+export const sendEmailOtpApi = async (
+ payload: SendEmailOtpPayload,
+): Promise => {
+ const { data } = await apiClient.post('/User/SendEmailOtp', payload);
+ return data;
+};
+
+export const confirmEmailOtpApi = async (
+ payload: ConfirmEmailOtpPayload,
+): Promise => {
+ const { data } = await apiClient.post('/User/ConfirmEmailOtp', payload);
+ return data;
+};
+
+export const completeUserInformationApi = async (
+ payload: CompleteUserInfoPayload,
+): Promise => {
+ const { data } = await apiClient.post(
+ '/User/CompleteUserInformation',
+ payload,
+ );
+ return data;
+};
diff --git a/src/features/authentication/components/DateOfBirth.tsx b/src/features/authentication/components/DateOfBirth.tsx
new file mode 100644
index 0000000..f219589
--- /dev/null
+++ b/src/features/authentication/components/DateOfBirth.tsx
@@ -0,0 +1,72 @@
+import { useMemo } from 'react';
+import {
+ DatePicker,
+ PickersDay,
+ type PickersDayProps,
+} from '@mui/x-date-pickers';
+import { LocalizationProvider } from '@mui/x-date-pickers';
+import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
+import { AdapterDateFnsJalali } from '@mui/x-date-pickers/AdapterDateFnsJalali';
+import { enUS } from 'date-fns/locale';
+import { faIR as faIRJalali } from 'date-fns-jalali/locale';
+import { getDay } from 'date-fns-jalali';
+import { format as formatJalali } from 'date-fns-jalali';
+import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
+import { toLocaleDigits } from '@/utils/persianDigit';
+import { type DateOfBirthProps } from '../types/settingForm';
+
+export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
+ const { t, i18n } = useTranslation('completionForm');
+ const isFarsi = i18n.language === 'fa' || i18n.language === 'fa-IR';
+
+ const { Adapter, locale, formatString, dayOfWeekFormatter } = useMemo(() => {
+ if (isFarsi) {
+ const persianDays = ['ی', 'د', 'س', 'چ', 'پ', 'ج', 'ش'];
+ return {
+ Adapter: AdapterDateFnsJalali,
+ locale: faIRJalali,
+ formatString: 'yyyy/MM/dd',
+ dayOfWeekFormatter: (date: Date) => persianDays[getDay(date)],
+ };
+ }
+ return {
+ Adapter: AdapterDateFns,
+ locale: enUS,
+ formatString: 'MM/dd/yyyy',
+ dayOfWeekFormatter: undefined,
+ };
+ }, [isFarsi]);
+
+ const CustomDay = (props: PickersDayProps) => {
+ const dayNumber = isFarsi
+ ? formatJalali(props.day, 'dd')
+ : format(props.day, 'dd');
+ return (
+
+ {toLocaleDigits(dayNumber, i18n.language)}
+
+ );
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/src/features/authentication/components/EmailSection.tsx b/src/features/authentication/components/EmailSection.tsx
new file mode 100644
index 0000000..022126d
--- /dev/null
+++ b/src/features/authentication/components/EmailSection.tsx
@@ -0,0 +1,184 @@
+import React from 'react';
+import {
+ TextField,
+ Box,
+ Button,
+ Switch,
+ FormGroup,
+ Typography,
+ InputAdornment,
+ IconButton,
+ CircularProgress,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { TickCircle, Edit } from 'iconsax-react';
+import { Icon } from '@rkheftan/harmony-ui';
+import { type EmailSectionProps } from '../types/settingForm';
+
+export function EmailSection({
+ showEmail,
+ setShowEmail,
+ email,
+ setEmail,
+ correctEmail,
+ codeSent,
+ verificationCode,
+ setVerificationCode,
+ buttonState,
+ getButtonLabel,
+ handleSendCode,
+ handleVerifyCode,
+ emailVerified,
+ loading,
+ handleEditEmail,
+}: EmailSectionProps) {
+ const { t } = useTranslation('completionForm');
+
+ const onSendCodeClick = () => {
+ if (!correctEmail) return;
+ handleSendCode();
+ };
+
+ const handleToggleEmail = (e: React.ChangeEvent) => {
+ setShowEmail(e.target.checked);
+ };
+
+ return (
+ <>
+
+
+
+
+ {t('completion.determineEmail')}
+
+
+
+ {showEmail && (
+
+
+ setEmail(e.target.value)}
+ error={email.length > 0 && !correctEmail}
+ sx={{ flex: '1 1 260px' }}
+ slotProps={{
+ input: {
+ startAdornment:
+ !loading && emailVerified ? (
+
+
+
+ ) : null,
+ endAdornment:
+ buttonState === 'counting' ? (
+
+
+
+
+
+ ) : null,
+ sx: {
+ paddingLeft: buttonState === 'counting' ? 0 : undefined,
+ },
+ },
+ }}
+ />
+ {!loading && !emailVerified && (
+
+ )}
+
+ {email && (
+
+ {correctEmail ? '' : t('completion.emailCorrectForm')}
+
+ )}
+ {!emailVerified && codeSent && correctEmail && (
+
+ setVerificationCode(e.target.value)}
+ sx={{ flex: '1 1 260px' }}
+ disabled={loading}
+ />
+
+
+ )}
+
+ )}
+ >
+ );
+}
diff --git a/src/features/authentication/components/PasswordSection.tsx b/src/features/authentication/components/PasswordSection.tsx
new file mode 100644
index 0000000..f8da4e9
--- /dev/null
+++ b/src/features/authentication/components/PasswordSection.tsx
@@ -0,0 +1,250 @@
+import React, { useState } from 'react';
+import {
+ TextField,
+ Box,
+ IconButton,
+ Switch,
+ FormGroup,
+ Typography,
+ InputAdornment,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { TickCircle, Eye, EyeSlash, CloseCircle } from 'iconsax-react';
+import { PasswordValidationItem } from './PasswordValidation';
+import { Icon } from '@rkheftan/harmony-ui';
+import { type PasswordSectionProps } from '../types/settingForm';
+
+export function PasswordSection({
+ showPasswordSection,
+ setShowPasswordSection,
+ password,
+ setPassword,
+ confirmPassword,
+ setConfirmPassword,
+ matchPassword,
+ hasNumber,
+ hasMinLength,
+ hasUpperAndLower,
+ hasSpecialChar,
+ validPassword,
+ showValidations,
+}: PasswordSectionProps) {
+ const { t } = useTranslation('completionForm');
+ const [showPasswordText, setShowPasswordText] = useState(false);
+ const [showPasswordRepetitionText, setShowPasswordRepetitionText] =
+ useState(false);
+
+ const handleTogglePasswordSection = (
+ e: React.ChangeEvent,
+ ) => {
+ setShowPasswordSection(e.target.checked);
+ };
+
+ const handleTogglePasswordEye = () => setShowPasswordText((prev) => !prev);
+ const handleTogglePasswordRepetitionEye = () =>
+ setShowPasswordRepetitionText((prev) => !prev);
+
+ return (
+ <>
+
+
+
+
+ {t('completion.determinePassword')}
+
+
+
+
+ {showPasswordSection && (
+
+
+ setPassword(e.target.value)}
+ variant="outlined"
+ type={showPasswordText ? 'text' : 'password'}
+ sx={{
+ flex: '1 1 260px',
+ '& .MuiInputBase-input': {
+ pr: 8, // Increased padding to accommodate both icons
+ },
+ }}
+ InputProps={{
+ endAdornment: (
+
+
+
+ {showPasswordText ? (
+
+ ) : (
+
+ )}
+
+ {validPassword && (
+
+ )}
+
+
+ ),
+ }}
+ />
+
+ setConfirmPassword(e.target.value)}
+ error={confirmPassword.length > 0 && !matchPassword}
+ helperText={
+ confirmPassword.length > 0 && !matchPassword
+ ? t('completion.notCompatibility')
+ : ' '
+ }
+ type={showPasswordRepetitionText ? 'text' : 'password'}
+ sx={{
+ flex: '1 1 260px',
+ }}
+ InputProps={{
+ endAdornment: (
+
+
+
+ {showPasswordRepetitionText ? (
+
+ ) : (
+
+ )}
+
+ {confirmPassword.length > 0 && (
+
+ )}
+
+
+ ),
+ }}
+ />
+
+
+ {password && showValidations && (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )}
+ >
+ );
+}
diff --git a/src/features/authentication/components/PasswordValidation.tsx b/src/features/authentication/components/PasswordValidation.tsx
new file mode 100644
index 0000000..9f482d7
--- /dev/null
+++ b/src/features/authentication/components/PasswordValidation.tsx
@@ -0,0 +1,39 @@
+import { Box, Typography } from '@mui/material';
+import { TickCircle } from 'iconsax-react';
+import { Icon } from '@rkheftan/harmony-ui';
+import { type ValidationItemProps } from '../types/settingForm';
+
+export function PasswordValidationItem({
+ isValid,
+ label,
+}: ValidationItemProps) {
+ return (
+
+
+
+
+ {label}
+
+
+ );
+}
diff --git a/src/features/authentication/components/PersonalInfoFields.tsx b/src/features/authentication/components/PersonalInfoFields.tsx
new file mode 100644
index 0000000..1f1d8e1
--- /dev/null
+++ b/src/features/authentication/components/PersonalInfoFields.tsx
@@ -0,0 +1,168 @@
+import {
+ TextField,
+ FormControl,
+ InputLabel,
+ MenuItem,
+ Select,
+ Box,
+ Autocomplete,
+ type SelectChangeEvent,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { Woman, Man } from 'iconsax-react';
+import DateOfBirth from './DateOfBirth';
+import { countries } from '../data/Countries';
+import { CountryFlag } from '@/components/CountryFlag';
+import { Icon } from '@rkheftan/harmony-ui';
+import { type PersonalInfoFieldsProps } from '../types/settingForm';
+import { Gender } from '../types/settingForm';
+
+export function PersonalInfoFields({
+ firstName,
+ setFirstName,
+ lastName,
+ setLastName,
+ nationalId,
+ setNationalId,
+ birthDate,
+ setBirthDate,
+ sex,
+ setSex,
+ country,
+ setCountry,
+}: PersonalInfoFieldsProps) {
+ const { t } = useTranslation('completionForm');
+
+ const countryOptions = countries.map((c) => ({
+ code: c.code,
+ label: t(c.label, { ns: 'countries' }),
+ }));
+
+ const currentCountry = countryOptions.find((c) => c.code === country) || null;
+
+ const handleChangeSex = (e: SelectChangeEvent) => {
+ setSex(e.target.value as Gender);
+ };
+
+ const genderOptions = [
+ {
+ value: Gender.Female,
+ icon: Woman,
+ label: t('completion.woman'),
+ color: '#F50057',
+ },
+ {
+ value: Gender.Male,
+ icon: Man,
+ label: t('completion.man'),
+ color: '#0091EA',
+ },
+ ];
+
+ return (
+
+
+
+ setFirstName(e.target.value)}
+ sx={{ flex: '1 1 260px' }}
+ />
+ setLastName(e.target.value)}
+ sx={{ flex: '1 1 260px' }}
+ />
+
+
+
+
+ {t('completion.gender')}
+
+
+
+ setNationalId(e.target.value)}
+ variant="outlined"
+ sx={{ flex: '1 1 260px' }}
+ />
+
+
+
+ option.label}
+ value={currentCountry}
+ onChange={(_, newValue) => setCountry(newValue?.code || '')}
+ renderOption={(props, option) => (
+
+
+ {option.label}
+
+ )}
+ renderInput={(params) => (
+
+ )}
+ clearOnEscape
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/authentication/components/SubmitSection.tsx b/src/features/authentication/components/SubmitSection.tsx
new file mode 100644
index 0000000..b1c8c07
--- /dev/null
+++ b/src/features/authentication/components/SubmitSection.tsx
@@ -0,0 +1,89 @@
+import { useState } from 'react';
+import {
+ Box,
+ Button,
+ Typography,
+ Link,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { type SubmitProps } from '../types/settingForm';
+
+export function SubmitSection({ onSubmit, loading, error }: SubmitProps) {
+ const { t, i18n } = useTranslation('completionForm');
+ const [openDialog, setOpenDialog] = useState(false);
+
+ const handleOpenDialog = (e: React.MouseEvent) => {
+ e.preventDefault();
+ setOpenDialog(true);
+ };
+
+ const agreementText = t('completion.agreement');
+ return (
+ <>
+
+
+ {t('completion.agreementPart1')}
+
+ {t('completion.agreementLinkText')}
+
+ {t('completion.agreementPart2')}
+
+
+
+ {error && {error}}
+
+
+
+ >
+ );
+}
diff --git a/src/features/authentication/components/UserCompletionForm.tsx b/src/features/authentication/components/UserCompletionForm.tsx
new file mode 100644
index 0000000..5d220ef
--- /dev/null
+++ b/src/features/authentication/components/UserCompletionForm.tsx
@@ -0,0 +1,329 @@
+import { useEffect, useState } from 'react';
+import { Box, Typography } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import Logo from '@/components/Logo';
+import { PersonalInfoFields } from './PersonalInfoFields';
+import { PasswordSection } from './PasswordSection';
+import { EmailSection } from './EmailSection';
+import { SubmitSection } from './SubmitSection';
+import { useToast } from '@rkheftan/harmony-ui';
+import { regex } from '../../../utils/regex';
+import { toLocaleDigits } from '../../../utils/persianDigit';
+import i18n from '@/config/i18n';
+import { Gender } from '../types/settingForm';
+import { useApi } from '@/hooks/useApi';
+import {
+ sendEmailOtpApi,
+ confirmEmailOtpApi,
+ completeUserInformationApi,
+} from '../api/userCompletion';
+import {
+ type SendEmailOtpPayload,
+ type ConfirmEmailOtpPayload,
+ type CompleteUserInfoPayload,
+} from '../types/completionFormApiTypes';
+import { type ApiResponse } from '@/types/apiResponse';
+
+export function UserCompletionForm() {
+ const { t } = useTranslation('completionForm');
+ const showToast = useToast();
+
+ const [firstName, setFirstName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [nationalId, setNationalId] = useState('');
+ const [birthDate, setBirthDate] = useState(null);
+ const [sex, setSex] = useState(Gender.Female);
+ const [country, setCountry] = useState('');
+ const [showPasswordSection, setShowPasswordSection] = useState(false);
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [showEmail, setShowEmail] = useState(false);
+ const [email, setEmail] = useState('');
+ const [verificationCode, setVerificationCode] = useState('');
+
+ const [codeSent, setCodeSent] = useState(false);
+ const [buttonState, setButtonState] = useState<'default' | 'counting'>(
+ 'default',
+ );
+ const [countdown, setCountdown] = useState(0);
+ const [emailVerified, setEmailVerified] = useState(false);
+ const [showPasswordValidations, setShowPasswordValidations] = useState(false);
+
+ const {
+ hasNumber,
+ hasMinLength,
+ hasUpperAndLower,
+ hasSpecialChar,
+ validPassword,
+ correctEmail,
+ } = regex(password, email);
+ const matchPassword = password === confirmPassword;
+
+ const { execute: sendCode, data: sendCodeData } = useApi(
+ async (payload: SendEmailOtpPayload) => {
+ const result = await sendEmailOtpApi(payload);
+ const conformingResult: ApiResponse = {
+ ...result,
+ errorCode: result.errorCode || 0,
+ validations: [],
+ };
+ return { data: conformingResult };
+ },
+ );
+
+ const {
+ execute: verifyCode,
+ loading: isVerifyingCode,
+ data: verifyCodeData,
+ } = useApi(async (payload: ConfirmEmailOtpPayload) => {
+ const result = await confirmEmailOtpApi(payload);
+ const conformingResult: ApiResponse = {
+ ...result,
+ errorCode: result.errorCode || 0,
+ validations: [],
+ };
+ return { data: conformingResult };
+ });
+
+ const {
+ execute: submitForm,
+ loading: isSubmitting,
+ error: submitError,
+ data: submitData,
+ } = useApi(async (payload: CompleteUserInfoPayload) => {
+ const result = await completeUserInformationApi(payload);
+ const conformingResult: ApiResponse = {
+ ...result,
+ errorCode: result.errorCode || 0,
+ validations: [],
+ };
+ return { data: conformingResult };
+ });
+
+ const getErrorMessage = (error: unknown): string | null => {
+ if (!error) return null;
+ if (error instanceof Error) return error.message;
+ return String(error);
+ };
+
+ useEffect(() => {
+ if (sendCodeData) {
+ if (sendCodeData.success) {
+ showToast({
+ message: sendCodeData.message || t('completion.successfullCodeSent'),
+ severity: 'success',
+ });
+ setCodeSent(true);
+ setButtonState('counting');
+ setCountdown(120);
+ } else {
+ showToast({
+ message: sendCodeData.message || t('completion.problem'),
+ severity: 'error',
+ });
+ }
+ }
+ }, [sendCodeData, showToast, t]);
+
+ useEffect(() => {
+ if (verifyCodeData) {
+ if (verifyCodeData.success) {
+ setEmailVerified(true);
+ showToast({
+ message: verifyCodeData.message || t('completion.codeVerified'),
+ severity: 'success',
+ });
+ } else {
+ showToast({
+ message: verifyCodeData.message || t('completion.invalidCode'),
+ severity: 'error',
+ });
+ setEmailVerified(false);
+ }
+ }
+ }, [verifyCodeData, showToast, t]);
+
+ useEffect(() => {
+ if (submitData) {
+ showToast({
+ message:
+ submitData.message ||
+ t(
+ submitData.success
+ ? 'completion.submitSuccess'
+ : 'completion.submitError',
+ ),
+ severity: submitData.success ? 'success' : 'error',
+ });
+ } else if (submitError) {
+ showToast({
+ message: getErrorMessage(submitError) || t('completion.problem'),
+ severity: 'error',
+ });
+ }
+ }, [submitData, submitError, showToast, t]);
+
+ useEffect(() => {
+ setShowPasswordValidations(password ? !validPassword : false);
+ }, [password, validPassword]);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (buttonState === 'counting' && countdown > 0) {
+ timer = setInterval(() => {
+ setCountdown((prev) => {
+ if (prev <= 1) {
+ setButtonState('default');
+ clearInterval(timer);
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ }
+ return () => clearInterval(timer);
+ }, [buttonState, countdown]);
+
+ const handleSendCode = () => {
+ sendCode({ email });
+ };
+
+ const handleVerifyCode = () => {
+ if (!verificationCode.trim()) {
+ showToast({
+ message: 'Please enter the verification code',
+ severity: 'warning',
+ });
+ return;
+ }
+ verifyCode({ email, otpCode: verificationCode });
+ };
+
+ const handleSubmit = () => {
+ submitForm({
+ userId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
+ firstName,
+ lastName,
+ gender: sex,
+ nationalId,
+ savePassword: showPasswordSection,
+ password: showPasswordSection ? password : undefined,
+ saveEmail: showEmail,
+ email: showEmail ? email : undefined,
+ birthDate,
+ country,
+ });
+ };
+
+ const getButtonLabel = () => {
+ if (buttonState === 'counting') {
+ const m = String(Math.floor(countdown / 60)).padStart(2, '0');
+ const s = String(countdown % 60).padStart(2, '0');
+ return toLocaleDigits(`${m}:${s}`, i18n.language);
+ }
+ return t('completion.vericationCodeButton');
+ };
+
+ const handleEditEmail = () => {
+ setButtonState('default');
+ setCodeSent(false);
+ setEmailVerified(false);
+ setVerificationCode('');
+ };
+
+ return (
+
+
+
+
+
+
+
+ {t('completion.title')}
+
+
+ {t('completion.description')}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/authentication/data/Countries.ts b/src/features/authentication/data/Countries.ts
new file mode 100644
index 0000000..2b05f9e
--- /dev/null
+++ b/src/features/authentication/data/Countries.ts
@@ -0,0 +1,271 @@
+export interface Country {
+ code: string;
+ label: string;
+ phone: string;
+}
+
+export interface Country {
+ code: string;
+ label: string;
+ phone: string;
+}
+
+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' },
+];
diff --git a/src/features/authentication/types/completionFormApiTypes.ts b/src/features/authentication/types/completionFormApiTypes.ts
new file mode 100644
index 0000000..303deca
--- /dev/null
+++ b/src/features/authentication/types/completionFormApiTypes.ts
@@ -0,0 +1,32 @@
+export interface GenericApiResponse {
+ success: boolean;
+ message: string;
+ errorCode?: number;
+}
+
+export interface SendEmailOtpPayload {
+ email: string;
+}
+
+export interface ConfirmEmailOtpPayload {
+ email: string;
+ otpCode: string;
+}
+
+export interface CompleteUserInfoPayload {
+ userId: string;
+ firstName: string;
+ lastName: string;
+ gender: 0 | 1 | 2;
+ nationalId: string;
+ birthDate: Date | null;
+ country: string;
+ savePassword?: boolean;
+ password?: string;
+ saveEmail?: boolean;
+ email?: string;
+}
+
+export interface CompleteUserInfoResponse extends GenericApiResponse {
+ validations: { property: string; message: string }[] | null;
+}
diff --git a/src/features/authentication/types/settingForm.ts b/src/features/authentication/types/settingForm.ts
new file mode 100644
index 0000000..eb9453f
--- /dev/null
+++ b/src/features/authentication/types/settingForm.ts
@@ -0,0 +1,73 @@
+import { type Dispatch, type SetStateAction } from 'react';
+
+export enum Gender {
+ None = 0,
+ Female = 1,
+ Male = 2,
+}
+
+export interface DateOfBirthProps {
+ value: Date | null;
+ onChange: (date: Date | null) => void;
+}
+
+export interface EmailSectionProps {
+ showEmail: boolean;
+ setShowEmail: (checked: boolean) => void;
+ email: string;
+ setEmail: (email: string) => void;
+ correctEmail: boolean;
+ codeSent: boolean;
+ verificationCode: string;
+ setVerificationCode: (code: string) => void;
+ buttonState: 'default' | 'counting' | 'sent';
+ getButtonLabel: () => string;
+ handleSendCode: () => void;
+ handleVerifyCode: () => void;
+ emailVerified: boolean;
+ loading: boolean;
+ handleEditEmail: () => void;
+}
+
+export interface PasswordSectionProps {
+ showPasswordSection: boolean;
+ setShowPasswordSection: (checked: boolean) => void;
+ password: string;
+ setPassword: (password: string) => void;
+ confirmPassword: string;
+ setConfirmPassword: (confirmPassword: string) => void;
+ matchPassword: boolean;
+ hasNumber: boolean;
+ hasMinLength: boolean;
+ hasUpperAndLower: boolean;
+ hasSpecialChar: boolean;
+ validPassword: boolean;
+ showValidations: boolean;
+}
+
+export interface ValidationItemProps {
+ isValid: boolean;
+ label: string;
+}
+
+export interface PersonalInfoFieldsProps {
+ firstName: string;
+ setFirstName: (v: string) => void;
+ lastName: string;
+ setLastName: (v: string) => void;
+ sex: Gender;
+ setSex: Dispatch>;
+ country: string;
+ setCountry: (country: string) => void;
+ nationalId: string;
+ setNationalId: (v: string) => void;
+ birthDate: Date | null;
+ setBirthDate: (d: Date | null) => void;
+}
+
+export interface SubmitProps {
+ onSubmit: () => void;
+ loading: boolean;
+ error: string | null;
+ success: boolean;
+}
diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts
new file mode 100644
index 0000000..51b1dfe
--- /dev/null
+++ b/src/features/authorization/api/authorizationAPI.ts
@@ -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 = (
+ url: string,
+ body: Object | null,
+): FetchPromise => {
+ 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(
+ 'User/GetUserStatusByPhoneNumberOrEmail',
+ body,
+ );
+};
+
+export const loginOrSignUpWithOtp = async (body: LoginRequest) => {
+ return fetchRequest('User/LoginOrSignUpWithOtp', body);
+};
+
+export const loginWithPassword = async (body: PasswordLoginRequest) => {
+ return fetchRequest('User/LoginWithPassword', body);
+};
+
+export const sendSmsOtp = async (body: SendSmsOtpRequest) => {
+ return fetchRequest('User/SendSmsOtp', body);
+};
+
+export const sendEmailOtp = async (body: SendEmailOtpRequest) => {
+ return fetchRequest('User/SendEmailOtp', body);
+};
+
+export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => {
+ return fetchRequest('User/ConfirmSmsOtp', body);
+};
+
+export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => {
+ return fetchRequest('User/ConfirmEmailOtp', body);
+};
+
+export const resetPassword = async (body: ResetPasswordRequest) => {
+ return fetchRequest('User/ResetPassword', body);
+};
+
+export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => {
+ return fetchRequest('User/SendForgetPassCode', body);
+};
+
+export const confirmForgetPassCode = async (
+ body: ConfirmForgetPassCodeRequest,
+) => {
+ return fetchRequest('User/ConfirmForgetPassCode', body);
+};
+
+export const loginOrSignUpWithGoogle = async (
+ body: LoginOrSignUpWithGoogleRequest,
+) => {
+ return fetchRequest(
+ 'User/LoginOrSignUpWithGoogle',
+ body,
+ );
+};
+
+export const completeUserInformation = async (
+ body: CompleteUserInformationRequest,
+) => {
+ return fetchRequest('User/CompleteUserInformation', body);
+};
+
+export const logOut = async () => {
+ return fetchRequest('User/LogOut', {});
+};
diff --git a/src/features/authorization/components/AuthenticationCard.tsx b/src/features/authorization/components/AuthenticationCard.tsx
new file mode 100644
index 0000000..6c49575
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationCard.tsx
@@ -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 (
+ `calc(100% - ${t.spacing(2)})`,
+ maxWidth: '552px',
+ }}
+ >
+ {children}
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
new file mode 100644
index 0000000..e02bd3a
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
@@ -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('register');
+ const [authType, setAuthType] = useState('phone');
+ const [currentStep, setCurrentStep] = useState('emailOrPhone');
+ const [loginRegisterValue, setLoginRegisterValue] = useState('');
+ const [countryCode, setCountryCode] = useState('+98');
+ const [addPhoneCountryCode, setAddPhoneCountryCode] =
+ useState('+98');
+ const [addedPhoneNumberValue, setAddedPhoneNumberValue] =
+ useState('');
+
+ 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' && (
+
+ )}
+
+ {currentStep === 'verify' && (
+ setCurrentStep('emailOrPhone')}
+ authMode={authMode}
+ authType={authType}
+ value={loginRegisterValue}
+ />
+ )}
+
+ {currentStep === 'enterPassword' && (
+ setCurrentStep('emailOrPhone')}
+ onLoginWithOTP={() => setCurrentStep('verify')}
+ emailOrPhone={loginRegisterValue}
+ />
+ )}
+
+ {currentStep === 'addPhoneNumber' && (
+ setCurrentStep('addedPhoneNumberVerify')}
+ />
+ )}
+
+ {currentStep === 'addedPhoneNumberVerify' && (
+ setCurrentStep('addPhoneNumber')}
+ value={addedPhoneNumberValue}
+ onPhoneNumberVerified={handlePhoneNumberVerified}
+ />
+ )}
+ >
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx
new file mode 100644
index 0000000..132f136
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx
@@ -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;
+ countryCode: CountryCode;
+ setCountryCode: Dispatch;
+ onCompleteSignUp: (countryCode: string, value: string) => void;
+}
+
+export const CompleteSignUp = ({
+ email,
+ value,
+ setValue,
+ countryCode,
+ setCountryCode,
+ onCompleteSignUp,
+}: CompleteSignUpProps) => {
+ const { t } = useTranslation('authentication');
+ const [error, setError] = useState();
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+ const [touched, setTouched] = useState(false);
+ const inputError: boolean = touched && !!error;
+ const [sendOtpLoading, setSendOtpLoading] = useState(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 (
+
+
+ {t('completeSignUp.completeSignUp')}
+
+
+
+ {t(
+ 'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
+ { email },
+ )}
+
+
+ setValue(e.target.value)}
+ onBlur={handleBlur}
+ error={inputError}
+ helperText={inputError ? error : ''}
+ autoFocus
+ slotProps={{
+ htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
+ input: {
+ endAdornment: (
+
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
new file mode 100644
index 0000000..d5b9b89
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
@@ -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('');
+ const [inputTouched, setInputTouched] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const inputRef = useRef(null);
+ const [loginLoading, setLoginLoading] = useState(false);
+ const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState();
+ const [loginAlertOpen, setLoginAlertOpen] = useState(false);
+ const [loginFailedMessage, setLoginFailedMessage] = useState('');
+ const [sendOtpLoading, setSendOtpLoading] = useState(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 (
+
+ setLoginAlertOpen(false)}
+ color={!isLoginStatusSuccess ? 'error' : 'success'}
+ >
+ {!isLoginStatusSuccess
+ ? loginFailedMessage
+ : t('verify.youHaveSuccessfullyLoggedIn')}
+
+
+
+
+ {t('enterPassword.loginWithPassword')}
+
+
+ }
+ onClick={onEditValue}
+ >
+ {emailOrPhone}
+
+
+
+
+ {t('enterPassword.enterThePasswordYouSetForYourAccount')}
+
+
+ setPassValue(e.target.value)}
+ onBlur={handleBlur}
+ error={!passValue && inputTouched}
+ helperText={
+ !passValue && inputTouched ? t('loginForm.thisFieldIsRequired') : ''
+ }
+ autoFocus
+ slotProps={{
+ htmlInput: { sx: { lineHeight: 1.5 } },
+ input: {
+ endAdornment: (
+ setShowPassword(!showPassword)}
+ >
+ {showPassword ? : }
+
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+ }
+ >
+ {t('enterPassword.loginWithOneTimeCode')}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
new file mode 100644
index 0000000..9a74826
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
@@ -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(false);
+
+ const clientRef = useRef(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 (
+ }
+ >
+ {t('loginForm.loginWithGoogle')}
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx
new file mode 100644
index 0000000..0b7e1aa
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx
@@ -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;
+ countryCode: CountryCode;
+ setCountryCode: Dispatch;
+ authType: AuthType;
+ setAuthType: Dispatch;
+ 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(false);
+ const { t } = useTranslation('authentication');
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+ const [error, setError] = useState();
+ const [touched, setTouched] = useState(false);
+ const [errorMessage, setErrorMessage] = useState();
+ const inputError: boolean = touched && !!error;
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ 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 (
+
+ setErrorMessage(undefined)}
+ open={!!errorMessage}
+ >
+ {errorMessage}
+
+
+
+ {t('loginForm.title')}
+
+ {t('loginForm.description')}
+
+
+
+
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx
new file mode 100644
index 0000000..25c0c26
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx
@@ -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('');
+ const [otpDigitInvalid, setOtpDigitInvalid] = useState(false);
+ const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
+ const [errorMessage, setErrorMessage] = useState();
+ const [verifyStatusLoading, setVerifyStatusLoading] =
+ useState(false);
+ const [verifyAlertOpen, setVerifyAlertOpen] = useState(false);
+ const { t } = useTranslation('authentication');
+ const [resendTimer, setResendTimer] = useState(120);
+ const [canResend, setCanResend] = useState(false);
+ const [resendLoading, setResendLoading] = useState(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 (
+
+
+ setVerifyAlertOpen(false)}
+ color={verifyStatus === 'failed' ? 'error' : 'success'}
+ >
+ {toastMessage}
+
+
+
+ {t('verify.verify')}
+
+ }
+ onClick={onEditValue}
+ >
+ {authType === 'phone' ? countryCode + value : value}
+
+
+
+
+ {otpMessage()}
+
+
+ setOtpCode(value)}
+ />
+
+
+
+
+
+ {t('verify.resendCodeIn')}
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx
new file mode 100644
index 0000000..ad937d4
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx
@@ -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('');
+ const [otpDigitInvalid, setOtpDigitInvalid] = useState(false);
+ const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
+ const [errorMessage, setErrorMessage] = useState();
+ const [verifyStatusLoading, setVerifyStatusLoading] =
+ useState(false);
+ const [verifyAlertOpen, setVerifyAlertOpen] = useState(false);
+ const { t } = useTranslation('authentication');
+ const [resendTimer, setResendTimer] = useState(120);
+ const [canResend, setCanResend] = useState(false);
+ const [resendLoading, setResendLoading] = useState(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 (
+
+
+ setVerifyAlertOpen(false)}
+ color={verifyStatus === 'failed' ? 'error' : 'success'}
+ >
+ {verifyAlertMessage()}
+
+
+
+ {t('verify.verify')}
+
+ }
+ onClick={onEditValue}
+ >
+ {countryCode + value}
+
+
+
+
+ {t(
+ 'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
+ )}
+
+
+ setOtpCode(value)}
+ />
+
+
+
+
+
+ {t('verify.resendCodeIn')}
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/CountryCodeSelector.tsx b/src/features/authorization/components/CountryCodeSelector.tsx
new file mode 100644
index 0000000..a93168b
--- /dev/null
+++ b/src/features/authorization/components/CountryCodeSelector.tsx
@@ -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;
+}
+
+/**
+ * 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);
+ const [searchTerm, setSearchTerm] = useState('');
+ const open = Boolean(anchorEl);
+ const searchInputRef = useRef(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 (
+
+
+ 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 */}
+
+
+
+ {value}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx
new file mode 100644
index 0000000..6da9c4d
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx
@@ -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('');
+ const [confirmPassValue, setConfirmPassValue] = useState('');
+ const [inputTouched, setInputTouched] = useState(false);
+ const [confirmInputTouched, setConfirmInputTouched] =
+ useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] =
+ useState(false);
+ const inputRef = useRef(null);
+ const confirmInputRef = useRef(null);
+ const [changePasswordLoading, setChangePasswordLoading] =
+ useState(false);
+ const [changePasswordStatus, setChangePasswordStatus] = useState<
+ 'success' | 'failed'
+ >();
+ const [changePassAlertOpen, setChangePassAlertOpen] =
+ useState(false);
+ const [changePassFailedMessage, setChangePassFailedMessage] =
+ useState('');
+
+ 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 (
+
+ setChangePassAlertOpen(false)}
+ color={changePasswordStatus === 'failed' ? 'error' : 'success'}
+ >
+ {changePasswordStatus === 'failed'
+ ? changePassFailedMessage
+ : t('forgetPassword.passwordChangedSuccessfully')}
+
+
+
+
+ {t('forgetPassword.changePassword')}
+
+
+ }
+ onClick={onEditInfo}
+ >
+ {forgettedPasswordInfo}
+
+
+
+
+ {t('forgetPassword.createANewPassword')}
+
+
+ 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 && (
+
+ ),
+ endAdornment: passValue ? (
+ setShowPassword(!showPassword)}
+ >
+ {showPassword ? : }
+
+ ) : (
+ ''
+ ),
+ },
+ }}
+ sx={{ mt: 4 }}
+ />
+
+ {!isValidPassword(passValue) && (
+
+ {passwordValidationRules.map((rule) => (
+
+
+
+ {rule.title}
+
+ ))}
+
+ )}
+
+ 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 && (
+
+ ),
+ endAdornment: confirmPassValue ? (
+ setShowConfirmPassword(!showConfirmPassword)}
+ >
+ {showConfirmPassword ? : }
+
+ ) : (
+ ''
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+
+
+
+
+ );
+};
diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx
new file mode 100644
index 0000000..137b413
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx
@@ -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('');
+ const [infoCountryCode, setInfoCountryCode] = useState('+98');
+ const [infoType, setInfoType] = useState('email');
+
+ const handleVerifyOtp = () => {
+ setForgetPassCurrentStep('verifyOtp');
+ };
+
+ const handleEditInfo = () => {
+ setForgetPassCurrentStep('enterInfo');
+ };
+
+ const handleOtpVerified = () => {
+ setForgetPassCurrentStep('setPassword');
+ };
+
+ const handlePasswordChanged = () => {
+ console.log('changingPasswordTo');
+ };
+
+ return (
+ <>
+ {forgetPassCurrentStep === 'enterInfo' && (
+
+ )}
+
+ {forgetPassCurrentStep === 'verifyOtp' && (
+
+ )}
+
+ {forgetPassCurrentStep === 'setPassword' && (
+
+ )}
+ >
+ );
+};
diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx
new file mode 100644
index 0000000..021aa81
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx
@@ -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('');
+ const [otpDigitInvalid, setOtpDigitInvalid] = useState(false);
+ const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>();
+ const [verifyStatusLoading, setVerifyStatusLoading] =
+ useState(false);
+ const [verifyAlertMessage, setVerifyAlertMessage] = useState();
+ const { t } = useTranslation('authentication');
+ const [resendTimer, setResendTimer] = useState(120);
+ const [canResend, setCanResend] = useState(false);
+ const [resendLoading, setResendLoading] = useState(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 (
+
+
+ setVerifyAlertMessage(undefined)}
+ color={'error'}
+ >
+ {verifyAlertMessage}
+
+
+
+
+ {t('forgetPassword.forgetPassword')}
+
+
+ }
+ onClick={onEditInfo}
+ >
+ {infoType === 'phone'
+ ? countryCode + forgettedPasswordInfo
+ : forgettedPasswordInfo}
+
+
+
+
+ {infoType === 'email'
+ ? t(
+ 'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
+ )
+ : t(
+ 'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
+ )}
+
+
+ setOtpCode(value)}
+ />
+
+
+
+
+
+ {t('verify.resendCodeIn')}
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx
new file mode 100644
index 0000000..6d2cf54
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx
@@ -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;
+ infoType: AuthType;
+ setInfoType: Dispatch;
+ onVerifyOtp: (value: string) => void;
+ countryCode: CountryCode;
+ setCountryCode: Dispatch;
+}
+
+export function ForgettedPasswordInfo({
+ forgettedPasswordInfo,
+ setForgettedPasswordInfo,
+ infoType,
+ setInfoType,
+ onVerifyOtp,
+ countryCode,
+ setCountryCode,
+}: ForgettedPasswordInfoProps) {
+ const { t } = useTranslation('authentication');
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+ const [error, setError] = useState();
+ const [touched, setTouched] = useState(false);
+ const [errorMessage, setErrorMessage] = useState();
+ const [sendCodeLoading, setSendCodeLoading] = useState(false);
+ const inputError: boolean = touched && !!error;
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ 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 (
+
+ setErrorMessage(undefined)}
+ open={!!errorMessage}
+ >
+ {errorMessage}
+
+
+
+
+ {t('forgetPassword.forgetPassword')}
+
+
+ {t(
+ 'forgetPassword.pleaseEnterYourMobileNumberEmailToRecoverYourPassword',
+ )}
+
+
+
+
+ ),
+ },
+ }}
+ sx={{ my: 4, mb: 8 }}
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/index.ts b/src/features/authorization/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/features/authorization/routes/AuthenticationPage.tsx b/src/features/authorization/routes/AuthenticationPage.tsx
new file mode 100644
index 0000000..47917cf
--- /dev/null
+++ b/src/features/authorization/routes/AuthenticationPage.tsx
@@ -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 (
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/routes/ForgetPasswordPage.tsx b/src/features/authorization/routes/ForgetPasswordPage.tsx
new file mode 100644
index 0000000..f54d961
--- /dev/null
+++ b/src/features/authorization/routes/ForgetPasswordPage.tsx
@@ -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 (
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/types/authTypes.ts b/src/features/authorization/types/authTypes.ts
new file mode 100644
index 0000000..91e45f5
--- /dev/null
+++ b/src/features/authorization/types/authTypes.ts
@@ -0,0 +1,10 @@
+export type AuthType = 'email' | 'phone';
+
+export type AuthMode = 'register' | 'login';
+
+export type AuthStep =
+ | 'emailOrPhone'
+ | 'verify'
+ | 'enterPassword'
+ | 'addPhoneNumber'
+ | 'addedPhoneNumberVerify';
diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts
new file mode 100644
index 0000000..99eb998
--- /dev/null
+++ b/src/features/authorization/types/userTypes.ts
@@ -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,
+}
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..3a11320
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,12 @@
+import React from 'react';
+
+declare global {
+ namespace JSX {
+ interface Element extends React.ReactElement {}
+ }
+
+ interface Window {
+ google: typeof google;
+ }
+ const google: any;
+}
diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts
index 3f17789..e1238b4 100644
--- a/src/hooks/useApi.ts
+++ b/src/hooks/useApi.ts
@@ -1,24 +1,46 @@
-import { useState } from 'react';
+import { useState, useEffect, useCallback } from 'react';
+import { type ApiResponse } from '@/types/apiResponse';
-type ApiFunction = (params?: P) => Promise<{ data: T }>;
+// Define options for the hook
+interface UseApiOptions {
+ // If true, the API call will be executed immediately on mount
+ immediate?: boolean;
+}
-export function useManualApi(apiFunction: ApiFunction) {
+export function useApi(
+ apiFunction: (...args: P) => Promise<{ data: T }>,
+ options: UseApiOptions = {},
+) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+
+ const execute = useCallback(
+ async (...args: P) => {
+ setLoading(true);
+ setError(null);
- const execute = async (params?: P) => {
- setLoading(true);
- setError(null);
- try {
- const response = await apiFunction(params);
- setData(response.data);
- } catch (err) {
- setError(err);
- } finally {
- setLoading(false);
+ try {
+ const response = await apiFunction(...args);
+
+ setData(response.data);
+ } catch (err) {
+ // TODO: can handle some common errors here, 400 and 500 errors
+ setError(err);
+ } finally {
+ setLoading(false);
+ }
+ },
+ [apiFunction],
+ );
+
+ // If the 'immediate' option is true, execute the function on mount
+ useEffect(() => {
+ if (options.immediate) {
+ // We pass undefined as params for the initial call.
+ execute(...(undefined as unknown as P));
}
- };
+ }, [execute, options.immediate]);
return { data, loading, error, execute };
}
diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts
index b2743a9..557ffe5 100644
--- a/src/lib/apiClient.ts
+++ b/src/lib/apiClient.ts
@@ -12,8 +12,8 @@ const apiClient = axios.create({
// Set default headers
headers: {
- // Accept: 'application/json',
- Authorization: 'Bearer ' + getToken(),
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
},
});
diff --git a/src/providers/AppProvider.tsx b/src/providers/AppProvider.tsx
index 9bea715..f68f2ae 100644
--- a/src/providers/AppProvider.tsx
+++ b/src/providers/AppProvider.tsx
@@ -3,6 +3,7 @@ import { I18nextProvider } from 'react-i18next';
import i18n from '@/config/i18n';
import { CustomThemeProvider } from './CustomThemeProvider';
import { RtlProvider } from './RtlProvider';
+import { ToastProvider } from '@rkheftan/harmony-ui';
export const AppProviders: React.FC<{ children: React.ReactNode }> = ({
children,
@@ -10,7 +11,9 @@ export const AppProviders: React.FC<{ children: React.ReactNode }> = ({
return (
- {children}
+
+ {children}
+
);
diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx
index 0b19f41..96b5960 100644
--- a/src/providers/CustomThemeProvider.tsx
+++ b/src/providers/CustomThemeProvider.tsx
@@ -13,6 +13,18 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
const direction = i18n.dir(i18n.language);
return createTheme({
+ components: {
+ MuiSelect: {
+ styleOverrides: {
+ root: {
+ textAlign: 'left',
+ },
+ select: {
+ textAlign: 'left',
+ },
+ },
+ },
+ },
direction: direction,
colorSchemes: {
light: {
@@ -25,6 +37,21 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
cssVariables: {
colorSchemeSelector: 'class',
},
+ components: {
+ MuiTextField: {
+ defaultProps: {
+ variant: 'outlined',
+ fullWidth: true,
+ },
+ },
+ MuiButton: {
+ defaultProps: {
+ size: 'large',
+ fullWidth: true,
+ variant: 'contained',
+ },
+ },
+ },
spacing: 8,
typography: typography,
components: {
@@ -40,7 +67,7 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
}, [i18n]);
return (
-
+
{children}
);
diff --git a/src/providers/RtlProvider.tsx b/src/providers/RtlProvider.tsx
index 7bbf52a..6550bb6 100644
--- a/src/providers/RtlProvider.tsx
+++ b/src/providers/RtlProvider.tsx
@@ -1,25 +1,23 @@
-import React, { useState, useEffect } from 'react';
+import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import rtlPlugin from 'stylis-plugin-rtl';
+import { prefixer } from 'stylis';
// This provider configures Emotion's cache to support RTL.
export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { i18n } = useTranslation();
- const [cache, setCache] = useState(createCache({ key: 'css' }));
- useEffect(() => {
- const newDir = i18n.dir(i18n.language);
-
- const newCache = createCache({
- key: 'css',
- stylisPlugins: newDir === 'rtl' ? [rtlPlugin] : [],
+ const cacheRtl = useMemo(() => {
+ const isRtl = i18n.dir(i18n.language) === 'rtl';
+ return createCache({
+ key: isRtl ? 'muirtl' : 'muiltr',
+ stylisPlugins: isRtl ? [prefixer, rtlPlugin] : [],
});
- setCache(newCache);
- }, [i18n, i18n.language]);
+ }, [i18n]);
- return {children};
+ return {children};
};
diff --git a/src/routes/config.tsx b/src/routes/config.tsx
new file mode 100644
index 0000000..61f2f8d
--- /dev/null
+++ b/src/routes/config.tsx
@@ -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: ,
+ },
+ {
+ path: '/setting',
+ element: ,
+ 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,
+ },
+ },
+ ],
+ },
+];
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 0000000..541a2fb
--- /dev/null
+++ b/src/routes/index.tsx
@@ -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 = (
+ Loading...}>{route.element}
+ );
+
+ // Conditionally wrap the element in the specified layout
+ // if (route.layout) {
+ // element = {element};
+ // }
+
+ // Conditionally wrap the element in the authentication guard
+ // if (route.authRequired) {
+ // element = {element};
+ // }
+
+ return {
+ path: route.path,
+ element: element,
+ ...(route.children && { children: mapRoutes(route.children) }),
+ };
+ });
+}
+
+export const router = createBrowserRouter(mapRoutes(appRoutes));
diff --git a/src/theme/colors.ts b/src/theme/colors.ts
index 219b302..4c68db7 100644
--- a/src/theme/colors.ts
+++ b/src/theme/colors.ts
@@ -4,7 +4,7 @@ import type { Palette } from './color.type';
export const PALETTE: Palette = {
primary: {
light: {
- main: blue[500],
+ main: blue[400],
dark: blue[700],
light: blue[100],
contrastText: '#FFFFFF',
@@ -49,9 +49,9 @@ export const PALETTE: Palette = {
},
error: {
light: {
- main: '#E53935',
- dark: '#C62828',
- light: '#EF5350',
+ main: '#d32f2f',
+ dark: '#c62828',
+ light: '#ef5350',
contrastText: '#FFFFFF',
},
dark: {
@@ -91,9 +91,9 @@ export const PALETTE: Palette = {
},
success: {
light: {
- main: '#43A047',
- dark: '#1B5E20',
- light: '#81C784',
+ main: '#2e7d32',
+ dark: '#1b5e20',
+ light: '#4caf50',
contrastText: '#FFFFFF',
},
// TODO
diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts
new file mode 100644
index 0000000..b6dee6b
--- /dev/null
+++ b/src/types/commonTypes.ts
@@ -0,0 +1,3 @@
+export type GUID = `${string}-${string}-${string}-${string}-${string}`;
+
+export type CountryCode = `+${number}`;
diff --git a/src/types/fetchPromise.ts b/src/types/fetchPromise.ts
new file mode 100644
index 0000000..84483d5
--- /dev/null
+++ b/src/types/fetchPromise.ts
@@ -0,0 +1,5 @@
+export type FetchPromise = Promise>;
+
+export interface FetchResponse extends Response {
+ json(): Promise;
+}
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
index a4c68f4..db060fe 100644
--- a/src/utils/regex.ts
+++ b/src/utils/regex.ts
@@ -3,6 +3,7 @@ export function regex(password: string) {
const hasMinLength = password.length >= 8;
const hasUpperAndLower = /[A-Z]/.test(password) && /[a-z]/.test(password);
const hasSpecialChar = /[!@#$%^&*]/.test(password);
+
return {
hasNumber,
diff --git a/src/utils/regexes/containsNumber.tsx b/src/utils/regexes/containsNumber.tsx
new file mode 100644
index 0000000..5ccefc8
--- /dev/null
+++ b/src/utils/regexes/containsNumber.tsx
@@ -0,0 +1 @@
+export const containsNumber = (value: string) => /\d/.test(value);
diff --git a/src/utils/regexes/containsSymbol.tsx b/src/utils/regexes/containsSymbol.tsx
new file mode 100644
index 0000000..c3ac510
--- /dev/null
+++ b/src/utils/regexes/containsSymbol.tsx
@@ -0,0 +1 @@
+export const containsSymbol = (value: string) => /[!@#$%&*\^]/.test(value);
diff --git a/src/utils/regexes/hasUpperAndLowerLetter.tsx b/src/utils/regexes/hasUpperAndLowerLetter.tsx
new file mode 100644
index 0000000..a29c262
--- /dev/null
+++ b/src/utils/regexes/hasUpperAndLowerLetter.tsx
@@ -0,0 +1,5 @@
+export const hasUpperAndLowerLetter = (value: string) => {
+ const hasUpper = /[A-Z]/.test(value);
+ const hasLower = /[a-z]/.test(value);
+ return hasUpper && hasLower;
+};
diff --git a/src/utils/regexes/isEmail.tsx b/src/utils/regexes/isEmail.tsx
new file mode 100644
index 0000000..b50768d
--- /dev/null
+++ b/src/utils/regexes/isEmail.tsx
@@ -0,0 +1,2 @@
+export const isEmail = (value: string) =>
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
diff --git a/src/utils/regexes/isNumeric.ts b/src/utils/regexes/isNumeric.ts
new file mode 100644
index 0000000..10e385d
--- /dev/null
+++ b/src/utils/regexes/isNumeric.ts
@@ -0,0 +1 @@
+export const isNumeric = (value: string) => /^\d*$/.test(value);
diff --git a/src/utils/regexes/isValidPhoneNumber.tsx b/src/utils/regexes/isValidPhoneNumber.tsx
new file mode 100644
index 0000000..c3d54cb
--- /dev/null
+++ b/src/utils/regexes/isValidPhoneNumber.tsx
@@ -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();
+};
diff --git a/src/utils/regexes/least8Chars.tsx b/src/utils/regexes/least8Chars.tsx
new file mode 100644
index 0000000..c4c4c8a
--- /dev/null
+++ b/src/utils/regexes/least8Chars.tsx
@@ -0,0 +1 @@
+export const least8Chars = (value: string) => value.length >= 8;
diff --git a/vite.config.d.ts b/vite.config.d.ts
index 340562a..2c646ae 100644
--- a/vite.config.d.ts
+++ b/vite.config.d.ts
@@ -1,2 +1,2 @@
-declare const _default: import("vite").UserConfig;
+declare const _default: import('vite').UserConfig;
export default _default;
diff --git a/vite.config.js b/vite.config.js
index 70ef262..e7bba8d 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -3,10 +3,10 @@ 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'),
- },
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
},
+ },
});