chore: optimization of code
This commit is contained in:
339
package-lock.json
generated
339
package-lock.json
generated
@@ -10,13 +10,17 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/material": "^7.2.0",
|
||||
"@mui/icons-material": "^7.3.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.4",
|
||||
"i18next": "^25.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"libphonenumber-js": "^1.12.10",
|
||||
"react": "^19.1.0",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@@ -28,8 +32,8 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
@@ -1254,32 +1258,58 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz",
|
||||
"integrity": "sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.1.tgz",
|
||||
"integrity": "sha512-+mIK1Z0BhOaQ0vCgOkT1mSrIpEHLo338h4/duuL4TBLXPvUMit732mnwJY3W40Avy30HdeSfwUAAGRkKmwRaEQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz",
|
||||
"integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==",
|
||||
"node_modules/@mui/icons-material": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.1.tgz",
|
||||
"integrity": "sha512-upzCtG6awpL6noEZlJ5Z01khZ9VnLNLaj7tb6iPbN6G97eYfUTs8e9OyPKy3rEms3VQWmVBfri7jzeaRxdFIzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@mui/core-downloads-tracker": "^7.2.0",
|
||||
"@mui/system": "^7.2.0",
|
||||
"@mui/types": "^7.4.4",
|
||||
"@mui/utils": "^7.2.0",
|
||||
"@babel/runtime": "^7.28.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@mui/material": "^7.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.1.tgz",
|
||||
"integrity": "sha512-Xf6Shbo03YmcBedZMwSpEFOwpYDtU7tC+rhAHTrA9FHk0FpsDqiQ9jUa1j/9s3HLs7KWb5mDcGnlwdh9Q9KAag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@mui/core-downloads-tracker": "^7.3.1",
|
||||
"@mui/system": "^7.3.1",
|
||||
"@mui/types": "^7.4.5",
|
||||
"@mui/utils": "^7.3.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.1.0",
|
||||
"react-is": "^19.1.1",
|
||||
"react-transition-group": "^4.4.5"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1292,7 +1322,7 @@
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@mui/material-pigment-css": "^7.2.0",
|
||||
"@mui/material-pigment-css": "^7.3.1",
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -1313,13 +1343,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.2.0.tgz",
|
||||
"integrity": "sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.1.tgz",
|
||||
"integrity": "sha512-WU3YLkKXii/x8ZEKnrLKsPwplCVE11yZxUvlaaZSIzCcI3x2OdFC8eMlNy74hVeUsYQvzzX1Es/k4ARPlFvpPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@mui/utils": "^7.2.0",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@mui/utils": "^7.3.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1340,12 +1370,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.2.0.tgz",
|
||||
"integrity": "sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.1.tgz",
|
||||
"integrity": "sha512-Nqo6OHjvJpXJ1+9TekTE//+8RybgPQUKwns2Lh0sq+8rJOUSUKS3KALv4InSOdHhIM9Mdi8/L7LTF1/Ky6D6TQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/serialize": "^1.3.3",
|
||||
"@emotion/sheet": "^1.4.0",
|
||||
@@ -1394,16 +1424,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz",
|
||||
"integrity": "sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.1.tgz",
|
||||
"integrity": "sha512-mIidecvcNVpNJMdPDmCeoSL5zshKBbYPcphjuh6ZMjhybhqhZ4mX6k9zmIWh6XOXcqRQMg5KrcjnO0QstrNj3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@mui/private-theming": "^7.2.0",
|
||||
"@mui/styled-engine": "^7.2.0",
|
||||
"@mui/types": "^7.4.4",
|
||||
"@mui/utils": "^7.2.0",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@mui/private-theming": "^7.3.1",
|
||||
"@mui/styled-engine": "^7.3.1",
|
||||
"@mui/types": "^7.4.5",
|
||||
"@mui/utils": "^7.3.1",
|
||||
"clsx": "^2.1.1",
|
||||
"csstype": "^3.1.3",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -1434,12 +1464,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz",
|
||||
"integrity": "sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==",
|
||||
"version": "7.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.5.tgz",
|
||||
"integrity": "sha512-ZPwlAOE3e8C0piCKbaabwrqZbW4QvWz0uapVPWya7fYj6PeDkl5sSJmomT7wjOcZGPB48G/a6Ubidqreptxz4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
"@babel/runtime": "^7.28.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -1451,17 +1481,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz",
|
||||
"integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.1.tgz",
|
||||
"integrity": "sha512-/31y4wZqVWa0jzMnzo6JPjxwP6xXy4P3+iLbosFg/mJQowL1KIou0LC+lquWW60FKVbKz5ZUWBg2H3jausa0pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@mui/types": "^7.4.4",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@mui/types": "^7.4.5",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^19.1.0"
|
||||
"react-is": "^19.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -1481,16 +1511,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.9.2.tgz",
|
||||
"integrity": "sha512-LXyRiF0U60enI/FMFPd0Ct9rP5PIIvUUWnTDqJ5IDSj7cDuD6/cG4E3RcV76iN9j+tALWMZKjeTkBGxQjZ078A==",
|
||||
"version": "8.10.0",
|
||||
"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",
|
||||
"@mui/x-internals": "8.9.2",
|
||||
"@mui/x-virtualizer": "0.1.0",
|
||||
"@mui/x-internals": "8.10.0",
|
||||
"@mui/x-virtualizer": "0.1.1",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
@@ -1557,6 +1586,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid-premium/node_modules/@mui/x-data-grid": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.9.2.tgz",
|
||||
"integrity": "sha512-LXyRiF0U60enI/FMFPd0Ct9rP5PIIvUUWnTDqJ5IDSj7cDuD6/cG4E3RcV76iN9j+tALWMZKjeTkBGxQjZ078A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@mui/utils": "^7.2.0",
|
||||
"@mui/x-internals": "8.9.2",
|
||||
"@mui/x-virtualizer": "0.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"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",
|
||||
"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-premium/node_modules/@mui/x-virtualizer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.1.0.tgz",
|
||||
"integrity": "sha512-fugd4tRJTTZpP2NFAxrngQQQhy1SDQq5wzMb6PJC1Y3b6THDw3P4vFuKxHmmfbPuCXLrxDifG0a8Q4LncBt1zA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.4",
|
||||
"@mui/x-internals": "8.9.2"
|
||||
},
|
||||
"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/@mui/x-data-grid-pro": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid-pro/-/x-data-grid-pro-8.9.2.tgz",
|
||||
@@ -1593,6 +1683,89 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid-pro/node_modules/@mui/x-data-grid": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.9.2.tgz",
|
||||
"integrity": "sha512-LXyRiF0U60enI/FMFPd0Ct9rP5PIIvUUWnTDqJ5IDSj7cDuD6/cG4E3RcV76iN9j+tALWMZKjeTkBGxQjZ078A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@mui/utils": "^7.2.0",
|
||||
"@mui/x-internals": "8.9.2",
|
||||
"@mui/x-virtualizer": "0.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"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",
|
||||
"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/node_modules/@mui/x-virtualizer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.1.0.tgz",
|
||||
"integrity": "sha512-fugd4tRJTTZpP2NFAxrngQQQhy1SDQq5wzMb6PJC1Y3b6THDw3P4vFuKxHmmfbPuCXLrxDifG0a8Q4LncBt1zA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.4",
|
||||
"@mui/x-internals": "8.9.2"
|
||||
},
|
||||
"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/@mui/x-data-grid/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-internals": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.9.2.tgz",
|
||||
@@ -1656,14 +1829,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-virtualizer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.1.0.tgz",
|
||||
"integrity": "sha512-fugd4tRJTTZpP2NFAxrngQQQhy1SDQq5wzMb6PJC1Y3b6THDw3P4vFuKxHmmfbPuCXLrxDifG0a8Q4LncBt1zA==",
|
||||
"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/x-internals": "8.9.2"
|
||||
"@mui/utils": "^7.2.0",
|
||||
"@mui/x-internals": "8.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -1677,6 +1850,28 @@
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-virtualizer/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/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -2129,18 +2324,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"version": "19.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
||||
"version": "19.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
|
||||
"integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -4374,6 +4569,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.10",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz",
|
||||
"integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
@@ -5035,9 +5236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz",
|
||||
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
|
||||
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
@@ -5178,8 +5379,7 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
@@ -5556,9 +5756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -5782,7 +5982,6 @@
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -13,13 +13,17 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/material": "^7.2.0",
|
||||
"@mui/icons-material": "^7.3.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.4",
|
||||
"i18next": "^25.3.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"libphonenumber-js": "^1.12.10",
|
||||
"react": "^19.1.0",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
@@ -31,8 +35,8 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
|
||||
247
public/locales/en/country.json
Normal file
247
public/locales/en/country.json
Normal file
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"country.afghanistan": "Afghanistan",
|
||||
"country.aland_islands": "Aland islands",
|
||||
"country.albania": "Albania",
|
||||
"country.algeria": "Algeria",
|
||||
"country.american_samoa": "American samoa",
|
||||
"country.andorra": "Andorra",
|
||||
"country.angola": "Angola",
|
||||
"country.anguilla": "Anguilla",
|
||||
"country.antarctica": "Antarctica",
|
||||
"country.antigua_and_barbuda": "Antigua and barbuda",
|
||||
"country.argentina": "Argentina",
|
||||
"country.armenia": "Armenia",
|
||||
"country.aruba": "Aruba",
|
||||
"country.australia": "Australia",
|
||||
"country.austria": "Austria",
|
||||
"country.azerbaijan": "Azerbaijan",
|
||||
"country.bahamas": "Bahamas",
|
||||
"country.bahrain": "Bahrain",
|
||||
"country.bangladesh": "Bangladesh",
|
||||
"country.barbados": "Barbados",
|
||||
"country.belarus": "Belarus",
|
||||
"country.belgium": "Belgium",
|
||||
"country.belize": "Belize",
|
||||
"country.benin": "Benin",
|
||||
"country.bermuda": "Bermuda",
|
||||
"country.bhutan": "Bhutan",
|
||||
"country.bolivia": "Bolivia",
|
||||
"country.bosnia_and_herzegovina": "Bosnia and herzegovina",
|
||||
"country.botswana": "Botswana",
|
||||
"country.brazil": "Brazil",
|
||||
"country.british_indian_ocean_territory": "British indian ocean territory",
|
||||
"country.british_virgin_islands": "British virgin islands",
|
||||
"country.brunei": "Brunei",
|
||||
"country.bulgaria": "Bulgaria",
|
||||
"country.burkina_faso": "Burkina faso",
|
||||
"country.burundi": "Burundi",
|
||||
"country.cambodia": "Cambodia",
|
||||
"country.cameroon": "Cameroon",
|
||||
"country.canada": "Canada",
|
||||
"country.cape_verde": "Cape verde",
|
||||
"country.cayman_islands": "Cayman islands",
|
||||
"country.central_african_republic": "Central african republic",
|
||||
"country.chad": "Chad",
|
||||
"country.chile": "Chile",
|
||||
"country.china": "China",
|
||||
"country.christmas_island": "Christmas island",
|
||||
"country.cocos_keeling_islands": "Cocos keeling islands",
|
||||
"country.colombia": "Colombia",
|
||||
"country.comoros": "Comoros",
|
||||
"country.congo_brazzaville": "Congo brazzaville",
|
||||
"country.congo_kinshasa": "Congo kinshasa",
|
||||
"country.cook_islands": "Cook islands",
|
||||
"country.costa_rica": "Costa rica",
|
||||
"country.cote_divoire": "Cote divoire",
|
||||
"country.croatia": "Croatia",
|
||||
"country.cuba": "Cuba",
|
||||
"country.curacao": "Curacao",
|
||||
"country.cyprus": "Cyprus",
|
||||
"country.czech_republic": "Czech republic",
|
||||
"country.denmark": "Denmark",
|
||||
"country.djibouti": "Djibouti",
|
||||
"country.dominica": "Dominica",
|
||||
"country.dominican_republic": "Dominican republic",
|
||||
"country.ecuador": "Ecuador",
|
||||
"country.egypt": "Egypt",
|
||||
"country.el_salvador": "El salvador",
|
||||
"country.equatorial_guinea": "Equatorial guinea",
|
||||
"country.eritrea": "Eritrea",
|
||||
"country.estonia": "Estonia",
|
||||
"country.eswatini": "Eswatini",
|
||||
"country.ethiopia": "Ethiopia",
|
||||
"country.falkland_islands": "Falkland islands",
|
||||
"country.faroe_islands": "Faroe islands",
|
||||
"country.fiji": "Fiji",
|
||||
"country.finland": "Finland",
|
||||
"country.france": "France",
|
||||
"country.french_guiana": "French guiana",
|
||||
"country.french_polynesia": "French polynesia",
|
||||
"country.gabon": "Gabon",
|
||||
"country.gambia": "Gambia",
|
||||
"country.georgia": "Georgia",
|
||||
"country.germany": "Germany",
|
||||
"country.ghana": "Ghana",
|
||||
"country.gibraltar": "Gibraltar",
|
||||
"country.greece": "Greece",
|
||||
"country.greenland": "Greenland",
|
||||
"country.grenada": "Grenada",
|
||||
"country.guadeloupe": "Guadeloupe",
|
||||
"country.guam": "Guam",
|
||||
"country.guatemala": "Guatemala",
|
||||
"country.guernsey": "Guernsey",
|
||||
"country.guinea": "Guinea",
|
||||
"country.guinea_bissau": "Guinea bissau",
|
||||
"country.guyana": "Guyana",
|
||||
"country.haiti": "Haiti",
|
||||
"country.honduras": "Honduras",
|
||||
"country.hong_kong": "Hong kong",
|
||||
"country.hungary": "Hungary",
|
||||
"country.iceland": "Iceland",
|
||||
"country.india": "India",
|
||||
"country.indonesia": "Indonesia",
|
||||
"country.iran": "Iran",
|
||||
"country.iraq": "Iraq",
|
||||
"country.ireland": "Ireland",
|
||||
"country.isle_of_man": "Isle of man",
|
||||
"country.israel": "Israel",
|
||||
"country.italy": "Italy",
|
||||
"country.jamaica": "Jamaica",
|
||||
"country.japan": "Japan",
|
||||
"country.jersey": "Jersey",
|
||||
"country.jordan": "Jordan",
|
||||
"country.kazakhstan": "Kazakhstan",
|
||||
"country.kenya": "Kenya",
|
||||
"country.kiribati": "Kiribati",
|
||||
"country.kosovo": "Kosovo",
|
||||
"country.kuwait": "Kuwait",
|
||||
"country.kyrgyzstan": "Kyrgyzstan",
|
||||
"country.laos": "Laos",
|
||||
"country.latvia": "Latvia",
|
||||
"country.lebanon": "Lebanon",
|
||||
"country.lesotho": "Lesotho",
|
||||
"country.liberia": "Liberia",
|
||||
"country.libya": "Libya",
|
||||
"country.liechtenstein": "Liechtenstein",
|
||||
"country.lithuania": "Lithuania",
|
||||
"country.luxembourg": "Luxembourg",
|
||||
"country.macau": "Macau",
|
||||
"country.madagascar": "Madagascar",
|
||||
"country.malawi": "Malawi",
|
||||
"country.malaysia": "Malaysia",
|
||||
"country.maldives": "Maldives",
|
||||
"country.mali": "Mali",
|
||||
"country.malta": "Malta",
|
||||
"country.marshall_islands": "Marshall islands",
|
||||
"country.martinique": "Martinique",
|
||||
"country.mauritania": "Mauritania",
|
||||
"country.mauritius": "Mauritius",
|
||||
"country.mayotte": "Mayotte",
|
||||
"country.mexico": "Mexico",
|
||||
"country.micronesia": "Micronesia",
|
||||
"country.moldova": "Moldova",
|
||||
"country.monaco": "Monaco",
|
||||
"country.mongolia": "Mongolia",
|
||||
"country.montenegro": "Montenegro",
|
||||
"country.montserrat": "Montserrat",
|
||||
"country.morocco": "Morocco",
|
||||
"country.mozambique": "Mozambique",
|
||||
"country.myanmar": "Myanmar",
|
||||
"country.namibia": "Namibia",
|
||||
"country.nauru": "Nauru",
|
||||
"country.nepal": "Nepal",
|
||||
"country.netherlands": "Netherlands",
|
||||
"country.new_caledonia": "New caledonia",
|
||||
"country.new_zealand": "New zealand",
|
||||
"country.nicaragua": "Nicaragua",
|
||||
"country.niger": "Niger",
|
||||
"country.nigeria": "Nigeria",
|
||||
"country.niue": "Niue",
|
||||
"country.norfolk_island": "Norfolk island",
|
||||
"country.north_korea": "North korea",
|
||||
"country.north_macedonia": "North macedonia",
|
||||
"country.northern_mariana_islands": "Northern mariana islands",
|
||||
"country.norway": "Norway",
|
||||
"country.oman": "Oman",
|
||||
"country.pakistan": "Pakistan",
|
||||
"country.palau": "Palau",
|
||||
"country.palestine": "Palestine",
|
||||
"country.panama": "Panama",
|
||||
"country.papua_new_guinea": "Papua new guinea",
|
||||
"country.paraguay": "Paraguay",
|
||||
"country.peru": "Peru",
|
||||
"country.philippines": "Philippines",
|
||||
"country.pitcairn_islands": "Pitcairn islands",
|
||||
"country.poland": "Poland",
|
||||
"country.portugal": "Portugal",
|
||||
"country.puerto_rico": "Puerto rico",
|
||||
"country.qatar": "Qatar",
|
||||
"country.reunion": "Reunion",
|
||||
"country.romania": "Romania",
|
||||
"country.russia": "Russia",
|
||||
"country.rwanda": "Rwanda",
|
||||
"country.saint_barthelemy": "Saint barthelemy",
|
||||
"country.saint_helena": "Saint helena",
|
||||
"country.saint_kitts_and_nevis": "Saint kitts and nevis",
|
||||
"country.saint_lucia": "Saint lucia",
|
||||
"country.saint_martin": "Saint martin",
|
||||
"country.saint_pierre_and_miquelon": "Saint pierre and miquelon",
|
||||
"country.saint_vincent_and_the_grenadines": "Saint vincent and the grenadines",
|
||||
"country.samoa": "Samoa",
|
||||
"country.san_marino": "San marino",
|
||||
"country.sao_tome_and_principe": "Sao tome and principe",
|
||||
"country.saudi_arabia": "Saudi arabia",
|
||||
"country.senegal": "Senegal",
|
||||
"country.serbia": "Serbia",
|
||||
"country.seychelles": "Seychelles",
|
||||
"country.sierra_leone": "Sierra leone",
|
||||
"country.singapore": "Singapore",
|
||||
"country.sint_maarten": "Sint maarten",
|
||||
"country.slovakia": "Slovakia",
|
||||
"country.slovenia": "Slovenia",
|
||||
"country.solomon_islands": "Solomon islands",
|
||||
"country.somalia": "Somalia",
|
||||
"country.south_africa": "South africa",
|
||||
"country.south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands",
|
||||
"country.south_korea": "South korea",
|
||||
"country.south_sudan": "South sudan",
|
||||
"country.spain": "Spain",
|
||||
"country.sri_lanka": "Sri lanka",
|
||||
"country.sudan": "Sudan",
|
||||
"country.suriname": "Suriname",
|
||||
"country.svalbard_and_jan_mayen": "Svalbard and jan mayen",
|
||||
"country.sweden": "Sweden",
|
||||
"country.switzerland": "Switzerland",
|
||||
"country.syria": "Syria",
|
||||
"country.taiwan": "Taiwan",
|
||||
"country.tajikistan": "Tajikistan",
|
||||
"country.tanzania": "Tanzania",
|
||||
"country.thailand": "Thailand",
|
||||
"country.timor_leste": "Timor leste",
|
||||
"country.togo": "Togo",
|
||||
"country.tokelau": "Tokelau",
|
||||
"country.tonga": "Tonga",
|
||||
"country.trinidad_and_tobago": "Trinidad and tobago",
|
||||
"country.tunisia": "Tunisia",
|
||||
"country.turkey": "Turkey",
|
||||
"country.turkmenistan": "Turkmenistan",
|
||||
"country.turks_and_caicos_islands": "Turks and caicos islands",
|
||||
"country.tuvalu": "Tuvalu",
|
||||
"country.us_virgin_islands": "Us virgin islands",
|
||||
"country.uganda": "Uganda",
|
||||
"country.ukraine": "Ukraine",
|
||||
"country.united_arab_emirates": "United arab emirates",
|
||||
"country.united_kingdom": "United kingdom",
|
||||
"country.united_states": "United states",
|
||||
"country.uruguay": "Uruguay",
|
||||
"country.uzbekistan": "Uzbekistan",
|
||||
"country.vanuatu": "Vanuatu",
|
||||
"country.vatican_city": "Vatican city",
|
||||
"country.venezuela": "Venezuela",
|
||||
"country.vietnam": "Vietnam",
|
||||
"country.wallis_and_futuna": "Wallis and futuna",
|
||||
"country.western_sahara": "Western sahara",
|
||||
"country.yemen": "Yemen",
|
||||
"country.zambia": "Zambia",
|
||||
"country.zimbabwe": "Zimbabwe"
|
||||
}
|
||||
@@ -38,6 +38,8 @@
|
||||
"phoneNumberText": "Your new contact number will replace your previous contact number.",
|
||||
"verb": ".",
|
||||
"notDetermined": "Not determined",
|
||||
"successfulChangePhone": "Phone number changed successfully"
|
||||
"successfulChangePhone": "Phone number changed successfully",
|
||||
"phoneNumberIsInvalid": "Phone number is invalid",
|
||||
"thisFieldIsRequired": "This field is requried"
|
||||
}
|
||||
}
|
||||
|
||||
182
public/locales/fa/country.json
Normal file
182
public/locales/fa/country.json
Normal file
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"country.afghanistan": "افغانستان",
|
||||
"country.aland_islands": "جزایر آلند",
|
||||
"country.albania": "آلبانی",
|
||||
"country.algeria": "الجزایر",
|
||||
"country.american_samoa": "ساموای آمریکایی",
|
||||
"country.andorra": "آندورا",
|
||||
"country.angola": "آنگولا",
|
||||
"country.anguilla": "آنگویلا",
|
||||
"country.antarctica": "جنوبگان",
|
||||
"country.antigua_and_barbuda": "آنتیگوا و باربودا",
|
||||
"country.argentina": "آرژانتین",
|
||||
"country.armenia": "ارمنستان",
|
||||
"country.aruba": "آروبا",
|
||||
"country.australia": "استرالیا",
|
||||
"country.austria": "اتریش",
|
||||
"country.azerbaijan": "آذربایجان",
|
||||
"country.bahamas": "باهاما",
|
||||
"country.bahrain": "بحرین",
|
||||
"country.bangladesh": "بنگلادش",
|
||||
"country.barbados": "باربادوس",
|
||||
"country.belarus": "بلاروس",
|
||||
"country.belgium": "بلژیک",
|
||||
"country.belize": "بلیز",
|
||||
"country.benin": "بنین",
|
||||
"country.bermuda": "برمودا",
|
||||
"country.bhutan": "بوتان",
|
||||
"country.bolivia": "بولیوی",
|
||||
"country.bosnia_and_herzegovina": "بوسنی و هرزگوین",
|
||||
"country.botswana": "بوتسوانا",
|
||||
"country.brazil": "برزیل",
|
||||
"country.british_virgin_islands": "جزایر ویرجین بریتانیا",
|
||||
"country.brunei": "برونئی",
|
||||
"country.bulgaria": "بلغارستان",
|
||||
"country.burkina_faso": "بورکینافاسو",
|
||||
"country.burundi": "بوروندی",
|
||||
"country.cambodia": "کامبوج",
|
||||
"country.cameroon": "کامرون",
|
||||
"country.canada": "کانادا",
|
||||
"country.cape_verde": "کیپ ورد",
|
||||
"country.cayman_islands": "جزایر کیمن",
|
||||
"country.central_african_republic": "جمهوری آفریقای مرکزی",
|
||||
"country.chad": "چاد",
|
||||
"country.chile": "شیلی",
|
||||
"country.china": "چین",
|
||||
"country.colombia": "کلمبیا",
|
||||
"country.comoros": "کومور",
|
||||
"country.costa_rica": "کاستاریکا",
|
||||
"country.cote_divoire": "ساحل عاج",
|
||||
"country.croatia": "کرواسی",
|
||||
"country.cuba": "کوبا",
|
||||
"country.cyprus": "قبرس",
|
||||
"country.czech_republic": "جمهوری چک",
|
||||
"country.denmark": "دانمارک",
|
||||
"country.djibouti": "جیبوتی",
|
||||
"country.dominica": "دومینیکا",
|
||||
"country.dominican_republic": "جمهوری دومینیکن",
|
||||
"country.ecuador": "اکوادور",
|
||||
"country.egypt": "مصر",
|
||||
"country.el_salvador": "السالوادور",
|
||||
"country.equatorial_guinea": "گینه استوایی",
|
||||
"country.eritrea": "اریتره",
|
||||
"country.estonia": "استونی",
|
||||
"country.eswatini": "سوازیلند",
|
||||
"country.ethiopia": "اتیوپی",
|
||||
"country.fiji": "فیجی",
|
||||
"country.finland": "فنلاند",
|
||||
"country.france": "فرانسه",
|
||||
"country.gabon": "گابن",
|
||||
"country.gambia": "گامبیا",
|
||||
"country.georgia": "گرجستان",
|
||||
"country.germany": "آلمان",
|
||||
"country.ghana": "غنا",
|
||||
"country.greece": "یونان",
|
||||
"country.guatemala": "گواتمالا",
|
||||
"country.guinea": "گینه",
|
||||
"country.guinea_bissau": "گینه بیسائو",
|
||||
"country.guyana": "گویان",
|
||||
"country.haiti": "هائیتی",
|
||||
"country.honduras": "هندوراس",
|
||||
"country.hungary": "مجارستان",
|
||||
"country.iceland": "ایسلند",
|
||||
"country.india": "هندوستان",
|
||||
"country.indonesia": "اندونزی",
|
||||
"country.iran": "ایران",
|
||||
"country.iraq": "عراق",
|
||||
"country.ireland": "ایرلند",
|
||||
"country.israel": "اسرائیل",
|
||||
"country.italy": "ایتالیا",
|
||||
"country.jamaica": "جامائیکا",
|
||||
"country.japan": "ژاپن",
|
||||
"country.jordan": "اردن",
|
||||
"country.kazakhstan": "قزاقستان",
|
||||
"country.kenya": "کنیا",
|
||||
"country.kuwait": "کویت",
|
||||
"country.kyrgyzstan": "قرقیزستان",
|
||||
"country.laos": "لائوس",
|
||||
"country.latvia": "لتونی",
|
||||
"country.lebanon": "لبنان",
|
||||
"country.lesotho": "لسوتو",
|
||||
"country.liberia": "لیبریا",
|
||||
"country.libya": "لیبی",
|
||||
"country.luxembourg": "لوکزامبورگ",
|
||||
"country.malaysia": "مالزی",
|
||||
"country.maldives": "مالدیو",
|
||||
"country.mali": "مالی",
|
||||
"country.malta": "مالت",
|
||||
"country.mauritania": "موریتانی",
|
||||
"country.mauritius": "موریس",
|
||||
"country.mexico": "مکزیک",
|
||||
"country.moldova": "مولداوی",
|
||||
"country.monaco": "موناکو",
|
||||
"country.mongolia": "مغولستان",
|
||||
"country.morocco": "مراکش",
|
||||
"country.mozambique": "موزامبیک",
|
||||
"country.myanmar": "میانمار",
|
||||
"country.namibia": "نامیبیا",
|
||||
"country.nepal": "نپال",
|
||||
"country.netherlands": "هلند",
|
||||
"country.new_zealand": "نیوزیلند",
|
||||
"country.nicaragua": "نیکاراگوئه",
|
||||
"country.niger": "نیجر",
|
||||
"country.nigeria": "نیجریه",
|
||||
"country.north_korea": "کره شمالی",
|
||||
"country.north_macedonia": "مقدونیه",
|
||||
"country.norway": "نروژ",
|
||||
"country.oman": "عمان",
|
||||
"country.pakistan": "پاکستان",
|
||||
"country.palau": "پالائو",
|
||||
"country.panama": "پاناما",
|
||||
"country.papua_new_guinea": "پاپوآ گینه نو",
|
||||
"country.paraguay": "پاراگوئه",
|
||||
"country.peru": "پرو",
|
||||
"country.philippines": "فیلیپین",
|
||||
"country.poland": "لهستان",
|
||||
"country.portugal": "پرتغال",
|
||||
"country.qatar": "قطر",
|
||||
"country.romania": "رومانی",
|
||||
"country.russia": "روسیه",
|
||||
"country.rwanda": "رواندا",
|
||||
"country.saudi_arabia": "عربستان سعودی",
|
||||
"country.senegal": "سنگال",
|
||||
"country.serbia": "صربستان",
|
||||
"country.seychelles": "سیشل",
|
||||
"country.sierra_leone": "سیرالئون",
|
||||
"country.singapore": "سنگاپور",
|
||||
"country.south_africa": "آفریقای جنوبی",
|
||||
"country.south_korea": "کره جنوبی",
|
||||
"country.south_sudan": "سودان جنوبی",
|
||||
"country.spain": "اسپانیا",
|
||||
"country.sri_lanka": "سریلانکا",
|
||||
"country.sudan": "سودان",
|
||||
"country.suriname": "سورینام",
|
||||
"country.sweden": "سوئد",
|
||||
"country.switzerland": "سوئیس",
|
||||
"country.syria": "سوریه",
|
||||
"country.taiwan": "تایوان",
|
||||
"country.tajikistan": "تاجیکستان",
|
||||
"country.tanzania": "تانزانیا",
|
||||
"country.thailand": "تایلند",
|
||||
"country.timor_leste": "تیمور شرقی",
|
||||
"country.togo": "توگو",
|
||||
"country.tonga": "تونگا",
|
||||
"country.trinidad_and_tobago": "ترینیداد و توباگو",
|
||||
"country.tunisia": "تونس",
|
||||
"country.turkey": "ترکیه",
|
||||
"country.turkmenistan": "ترکمنستان",
|
||||
"country.tuvalu": "تووالو",
|
||||
"country.uganda": "اوگاندا",
|
||||
"country.ukraine": "اوکراین",
|
||||
"country.united_arab_emirates": "امارات متحده عربی",
|
||||
"country.united_kingdom": "انگلستان",
|
||||
"country.united_states": "ایالات متحده آمریکا",
|
||||
"country.uruguay": "اروگوئه",
|
||||
"country.uzbekistan": "ازبکستان",
|
||||
"country.vanuatu": "وانواتو",
|
||||
"country.venezuela": "ونزوئلا",
|
||||
"country.vietnam": "ویتنام",
|
||||
"country.yemen": "یمن",
|
||||
"country.zambia": "زامبیا",
|
||||
"country.zimbabwe": "زیمبابوه"
|
||||
}
|
||||
@@ -38,6 +38,8 @@
|
||||
"phoneNumberText": "شماره تماس جدید شما جایگزین شماره تماس قبلی",
|
||||
"verb": "خواهد شد",
|
||||
"notDetermined": "تعیین نشده",
|
||||
"successfulChangePhone": "شماره تماس با موفقیت تغییر کرد"
|
||||
"successfulChangePhone": "شماره تماس با موفقیت تغییر کرد",
|
||||
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
|
||||
"thisFieldIsRequired": "این فیلد الزامی است"
|
||||
}
|
||||
}
|
||||
|
||||
47
src/App.tsx
47
src/App.tsx
@@ -1,4 +1,4 @@
|
||||
import { CssBaseline, useColorScheme } from '@mui/material';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import './App.css';
|
||||
import { LanguageManager } from './components/LanguageManager';
|
||||
import { Settings } from './features/profile/routes/SettingPage';
|
||||
@@ -9,53 +9,8 @@ function App() {
|
||||
<CssBaseline />
|
||||
<LanguageManager />
|
||||
<Settings />
|
||||
{/* <div style={{ padding: '16px' }}>
|
||||
<Typography variant="h3">{t('helloWorld')}</Typography>
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: '10px', mt: 5 }}
|
||||
>
|
||||
<Button color="secondary" variant="contained">
|
||||
secondary button
|
||||
</Button>
|
||||
<TextField label={t('helloWorld')} />
|
||||
<Alert severity="success" variant="filled">
|
||||
success
|
||||
</Alert>
|
||||
<Alert severity="warning" variant="filled">
|
||||
warning
|
||||
</Alert>
|
||||
<Alert severity="info" variant="filled">
|
||||
info
|
||||
</Alert>
|
||||
<Alert severity="error" variant="filled">
|
||||
error
|
||||
</Alert>
|
||||
</Box>
|
||||
</div> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
import { Button } from '@mui/material';
|
||||
import { PersonalInformation } from './features/profile/components/userInformation/PersonalInformation';
|
||||
import { PhoneNumber } from './features/profile/components/userInformation/PhoneNumber';
|
||||
import { SocialMedia } from './features/profile/components/userInformation/SocialMedia';
|
||||
import { PasswordSecurity } from './features/profile/components/security/PasswordSecurity';
|
||||
import { RecentLogins } from './features/profile/components/security/RecentLogins';
|
||||
import { ActiveDevices } from './features/profile/components/activeDevices/ActiveDevices';
|
||||
import { Setting } from './features/profile/components/setting/Setting';
|
||||
|
||||
export const ThemeToggleButton = () => {
|
||||
const { mode, setMode } = useColorScheme();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}
|
||||
>
|
||||
Switch to {mode === 'light' ? 'Dark' : 'Light'} Mode
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
254
src/features/profile/components/CountryCodeSelector.tsx
Normal file
254
src/features/profile/components/CountryCodeSelector.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import {
|
||||
Box,
|
||||
InputAdornment,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useMemo, useRef, useState, type RefObject } from 'react';
|
||||
import { ArrowDown2 } from 'iconsax-react';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { countries, type Country } from '../data/countries';
|
||||
|
||||
interface CountryCodeSelectorProps {
|
||||
show: boolean;
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
menuAnchor: HTMLElement | null;
|
||||
onCloseFocusRef: RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An animated country code adornment that fades and slides into view.
|
||||
* Its visibility is controlled by the `show` prop.
|
||||
*/
|
||||
export function CountryCodeSelector({
|
||||
show,
|
||||
value,
|
||||
onChange,
|
||||
menuAnchor,
|
||||
onCloseFocusRef,
|
||||
}: CountryCodeSelectorProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const open = Boolean(anchorEl);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const selectedCountry =
|
||||
countries.find((c) => c.phone === value) || countries[0];
|
||||
|
||||
const handleClick = () => {
|
||||
setAnchorEl(menuAnchor);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setTimeout(() => {
|
||||
setAnchorEl(null);
|
||||
}, 0);
|
||||
setTimeout(() => {
|
||||
onCloseFocusRef.current?.focus();
|
||||
}, 100);
|
||||
setSearchTerm(''); // Reset search on close
|
||||
};
|
||||
|
||||
const handleSelect = (country: Country) => {
|
||||
onChange(country.phone);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMenuEntered = () => {
|
||||
// Focus the input field after the menu has finished opening
|
||||
searchInputRef.current?.focus();
|
||||
};
|
||||
|
||||
const filteredCountries = useMemo(
|
||||
() =>
|
||||
countries.filter(
|
||||
(country) =>
|
||||
t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
country.phone.includes(searchTerm),
|
||||
),
|
||||
[searchTerm],
|
||||
);
|
||||
|
||||
return (
|
||||
<InputAdornment
|
||||
position={i18n.dir() === 'rtl' ? 'start' : 'end'}
|
||||
sx={{
|
||||
mx: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
// Animate width and opacity based on the 'show' prop
|
||||
width: show ? 'auto' : 0,
|
||||
opacity: show ? 1 : 0,
|
||||
transition: (theme) =>
|
||||
theme.transitions.create(['width', 'opacity'], {
|
||||
duration: theme.transitions.duration.standard,
|
||||
}),
|
||||
|
||||
// Prevent content from wrapping or spilling out during animation
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
// layout styles
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.25,
|
||||
pl: show ? 0.25 : 0,
|
||||
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* This inner Box prevents the content from being squeezed during the transition */}
|
||||
<ArrowDown2 size="24" variant="Bold" />
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.primary"
|
||||
component="div"
|
||||
sx={{ direction: 'rtl' }} // TODO: need to fixed for both en and fa
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
|
||||
<ReactCountryFlag
|
||||
countryCode={selectedCountry.code}
|
||||
svg
|
||||
style={{
|
||||
height: '1.5rem',
|
||||
width: '1.5rem',
|
||||
// TODO: Check alignment for better styling definition
|
||||
marginTop: '-2px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
slotProps={{
|
||||
list: {
|
||||
sx: {
|
||||
// Set the width to match the anchor element's width
|
||||
width: menuWidth,
|
||||
height: '18.75rem',
|
||||
},
|
||||
},
|
||||
transition: {
|
||||
onEntered: handleMenuEntered,
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
p: 1,
|
||||
backgroundColor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
inputRef={searchInputRef}
|
||||
size="small"
|
||||
fullWidth
|
||||
label={t('labels.search')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Can improve preformance with using virtual scrolling */}
|
||||
<Box sx={{ width: '100%', maxHeight: '14.75rem', overflow: 'auto' }}>
|
||||
{filteredCountries.length === 0 ? (
|
||||
<MenuItem disabled>
|
||||
<ListItem>
|
||||
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
) : (
|
||||
filteredCountries.map((country) => (
|
||||
<MenuItem
|
||||
key={country.code}
|
||||
selected={country.phone === value}
|
||||
onClick={() => handleSelect(country)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ReactCountryFlag
|
||||
countryCode={country.code}
|
||||
svg
|
||||
style={{
|
||||
height: '1.125rem',
|
||||
width: '1.125rem',
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(country.label)} />
|
||||
<Typography color="text.secondary">
|
||||
{country.phone}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* virtual scrolling */}
|
||||
{/* <Virtuoso
|
||||
style={{ height: '14.75rem' }} // Adjust height to account for the search bar
|
||||
data={filteredCountries}
|
||||
components={{
|
||||
EmptyPlaceholder: () => (
|
||||
<MenuItem disabled>
|
||||
<ListItemText>{t('messages.noResultFound')}</ListItemText>
|
||||
</MenuItem>
|
||||
),
|
||||
}}
|
||||
initialTopMostItemIndex={countries.indexOf(selectedCountry)}
|
||||
itemContent={(_, country) => (
|
||||
<MenuItem
|
||||
key={country.code}
|
||||
selected={country.phone === value}
|
||||
onClick={() => handleSelect(country)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ReactCountryFlag
|
||||
countryCode={country.code}
|
||||
svg
|
||||
style={{ fontSize: '1.25em', lineHeight: '1.25em' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={country.label} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{country.phone}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
/> */}
|
||||
</Menu>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { DeviceMessage, Logout } from 'iconsax-react';
|
||||
import Logo from '@/components/Logo';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import React from 'react';
|
||||
|
||||
export function ActiveDevices() {
|
||||
const { t } = useTranslation('activeDevices');
|
||||
@@ -83,9 +84,8 @@ export function ActiveDevices() {
|
||||
}}
|
||||
>
|
||||
{devices.map((device) => (
|
||||
<>
|
||||
<React.Fragment key={device.id}>
|
||||
<Box
|
||||
key={device.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
@@ -199,7 +199,7 @@ export function ActiveDevices() {
|
||||
{isXsup && (
|
||||
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
||||
)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Box>
|
||||
</CardContainer>
|
||||
|
||||
25
src/features/profile/components/layout/Header.tsx
Normal file
25
src/features/profile/components/layout/Header.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export function Header() {
|
||||
const headers = [{ name: 'محمدحسین برزه گر', phone: '09123456789' }];
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: 84,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
{headers.map((h) => (
|
||||
<Box key={h.phone}>
|
||||
<Typography variant="body2">{h.name}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{h.phone}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
127
src/features/profile/components/layout/Layout.tsx
Normal file
127
src/features/profile/components/layout/Layout.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useState } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Drawer,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { SideNav } from '@rkheftan/harmony-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HambergerMenu } from 'iconsax-react';
|
||||
|
||||
import { Header } from './Header';
|
||||
import { getNavConfig } from './navConfigs';
|
||||
|
||||
export function Layout() {
|
||||
const theme = useTheme();
|
||||
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const isLgUp = useMediaQuery(theme.breakpoints.up('lg'));
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation('sideMap');
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const navWidthMd = isLgUp ? 274 : 'min(15vw, 180px)';
|
||||
// const sidebarWidth = isLgUp ? 274 : isMdUp ? 'min(15vw, 180px)' : 0;
|
||||
|
||||
const navConfig = getNavConfig(t);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
px: { xs: 0, sm: 0, md: 'max(1vw, 8px)', lg: '60px' },
|
||||
py: { xs: 0, sm: 0, md: '40px', lg: '80px' },
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: {
|
||||
xs: '100vw',
|
||||
md: 'min(90vw, 900px)',
|
||||
lg: '1100px',
|
||||
},
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
bgcolor: 'background.paper',
|
||||
overflow: 'hidden',
|
||||
borderRadius: { xs: 0, md: 2 },
|
||||
boxShadow: { xs: 0, md: 3 },
|
||||
}}
|
||||
>
|
||||
{!isMdUp && (
|
||||
<AppBar position="static" elevation={0}>
|
||||
<Toolbar sx={{ backgroundColor: 'background.paper', gap: 2 }}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
size="medium"
|
||||
sx={{ backgroundColor: '#2979FF1F' }}
|
||||
>
|
||||
<HambergerMenu size={24} color="#1976d2" />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)}
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
{isMdUp && (
|
||||
<Box
|
||||
sx={{
|
||||
width: navWidthMd,
|
||||
flexShrink: 0,
|
||||
transition: 'width 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<SideNav
|
||||
navConfig={navConfig}
|
||||
header={<Header />}
|
||||
activePath={location.pathname + location.hash}
|
||||
sideNavVariant="full"
|
||||
positioning="absolute"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
px: { xs: 2, sm: 3, md: 'max(1.5vw, 12px)' },
|
||||
py: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{!isMdUp && (
|
||||
<Drawer
|
||||
anchor="left"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
>
|
||||
<Box width={navWidthMd}>
|
||||
<SideNav
|
||||
navConfig={navConfig}
|
||||
header={<Header />}
|
||||
activePath={location.pathname + location.hash}
|
||||
sideNavVariant="full"
|
||||
/>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
52
src/features/profile/components/layout/Routes.tsx
Normal file
52
src/features/profile/components/layout/Routes.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||
import { Layout } from './Layout';
|
||||
import { PersonalInformation } from '../userInformation/PersonalInformation';
|
||||
import { PhoneNumber } from '../userInformation/PhoneNumber';
|
||||
import { SocialMedia } from '../userInformation/SocialMedia';
|
||||
import { PasswordSecurity } from '../security/PasswordSecurity';
|
||||
import { RecentLogins } from '../security/RecentLogins';
|
||||
import { ActiveDevices } from '../activeDevices/ActiveDevices';
|
||||
import { Setting } from '../setting/Setting';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{ path: '/', element: <Navigate to="/profile" replace /> },
|
||||
{
|
||||
path: '/profile',
|
||||
element: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<div id="info">
|
||||
<PersonalInformation />
|
||||
</div>
|
||||
<div id="contact-info">
|
||||
<PhoneNumber />
|
||||
</div>
|
||||
<div id="email">
|
||||
<SocialMedia />
|
||||
</div>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/security',
|
||||
element: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<div id="password">
|
||||
<PasswordSecurity />
|
||||
</div>
|
||||
<div id="locations"></div>
|
||||
<div id="sessions">
|
||||
<RecentLogins />
|
||||
</div>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{ path: '/devices', element: <ActiveDevices /> },
|
||||
{ path: '/setting', element: <Setting /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
121
src/features/profile/components/layout/navConfigs.tsx
Normal file
121
src/features/profile/components/layout/navConfigs.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
Devices,
|
||||
LocationTick,
|
||||
Mobile,
|
||||
PasswordCheck,
|
||||
Personalcard,
|
||||
ProfileCircle,
|
||||
Setting as SettingIcon,
|
||||
Shield,
|
||||
Sms,
|
||||
} from 'iconsax-react';
|
||||
import { type TFunction } from 'i18next';
|
||||
|
||||
export function getNavConfig(t: TFunction) {
|
||||
return [
|
||||
{
|
||||
text: t('side.account'),
|
||||
getIcon: (selected: boolean) =>
|
||||
selected ? (
|
||||
<ProfileCircle size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<ProfileCircle size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile',
|
||||
children: [
|
||||
{
|
||||
text: t('side.personalInfo'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<Personalcard size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Personalcard size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile#info',
|
||||
},
|
||||
{
|
||||
text: t('side.phoneNumber'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<Mobile size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Mobile size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile#contact-info',
|
||||
},
|
||||
{
|
||||
text: t('side.email'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<Sms size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Sms size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile#email',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: t('side.security'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<Shield size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Shield size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security',
|
||||
children: [
|
||||
{
|
||||
text: t('side.password'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<PasswordCheck size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<PasswordCheck size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security#password',
|
||||
},
|
||||
{
|
||||
text: t('side.verifiedAddress'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<LocationTick size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<LocationTick size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security#locations',
|
||||
},
|
||||
{
|
||||
text: t('side.recentLogins'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<Devices size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Devices size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security#sessions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: t('side.activeDevices'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<Devices size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Devices size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/devices',
|
||||
},
|
||||
{
|
||||
text: t('side.settings'),
|
||||
getIcon: (sel: boolean) =>
|
||||
sel ? (
|
||||
<SettingIcon size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<SettingIcon size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/setting',
|
||||
},
|
||||
];
|
||||
}
|
||||
179
src/features/profile/components/security/PasswordDialog.tsx
Normal file
179
src/features/profile/components/security/PasswordDialog.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
IconButton,
|
||||
Box,
|
||||
Button,
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
import { CloseCircle, Refresh } from 'iconsax-react';
|
||||
import { PasswordValidationItem } from './PasswordValidation';
|
||||
|
||||
interface PasswordDialogProps {
|
||||
open: boolean;
|
||||
fullScreen: boolean;
|
||||
handleClose: () => void;
|
||||
password: string;
|
||||
setPassword: (val: string) => void;
|
||||
confirmPassword: string;
|
||||
setConfirmPassword: (val: string) => void;
|
||||
currentPassword: string;
|
||||
setCurrentPassword: (val: string) => void;
|
||||
showValidation: boolean;
|
||||
hasNumber: boolean;
|
||||
hasMinLength: boolean;
|
||||
hasUpperAndLower: boolean;
|
||||
hasSpecialChar: boolean;
|
||||
matchPassword: boolean;
|
||||
loading: boolean;
|
||||
handleShowAlert: () => void;
|
||||
changePassword: boolean;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export function PasswordDialog({
|
||||
open,
|
||||
fullScreen,
|
||||
handleClose,
|
||||
password,
|
||||
setPassword,
|
||||
confirmPassword,
|
||||
setConfirmPassword,
|
||||
currentPassword,
|
||||
setCurrentPassword,
|
||||
showValidation,
|
||||
hasNumber,
|
||||
hasMinLength,
|
||||
hasUpperAndLower,
|
||||
hasSpecialChar,
|
||||
matchPassword,
|
||||
loading,
|
||||
handleShowAlert,
|
||||
changePassword,
|
||||
t,
|
||||
}: PasswordDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
maxWidth="xs"
|
||||
scroll="body"
|
||||
PaperProps={{ sx: { mx: 1 } }}
|
||||
>
|
||||
<DialogTitle sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseCircle size={24} color="#82B1FF" />
|
||||
</IconButton>
|
||||
<Box component="span" fontWeight="bold" fontSize={18}>
|
||||
{t('securityForm.addPassword')}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{changePassword && (
|
||||
<>
|
||||
<TextField
|
||||
label={t('securityForm.currentPassword')}
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
/>
|
||||
<Link sx={{ fontSize: '15px' }}>
|
||||
{t('securityForm.forgetPassword')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
|
||||
{password && showValidation && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '364px', width: '100%' }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('securityForm.hasNumber')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('securityForm.hasMinLength')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('securityForm.hasUpperAndLower')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('securityForm.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.confirmPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={confirmPassword.length > 0 && !matchPassword}
|
||||
helperText={
|
||||
confirmPassword.length > 0 && !matchPassword
|
||||
? t('securityForm.notCompatibility')
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ maxWidth: '364px', height: 48, textTransform: 'none' }}
|
||||
variant="contained"
|
||||
onClick={handleShowAlert}
|
||||
disabled={!password || password !== confirmPassword || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
animation: 'spin 1s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Refresh size="20" color="#fff" />
|
||||
</Box>
|
||||
) : (
|
||||
t('securityForm.confirm')
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,23 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
TextField,
|
||||
DialogActions,
|
||||
IconButton,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Link,
|
||||
} from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CloseCircle, Refresh } from 'iconsax-react';
|
||||
import { PasswordValidationItem } from './PasswordValidation';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import Logo from '@/components/Logo';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import Logo from '@/components/Logo';
|
||||
import { PasswordDialog } from './PasswordDialog';
|
||||
import { Toast } from '@/components/Toast';
|
||||
|
||||
export function PasswordSecurity() {
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const { t } = useTranslation('security');
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@@ -42,21 +35,6 @@ export function PasswordSecurity() {
|
||||
hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
|
||||
const matchPassword = password === confirmPassword;
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
const handleShowAlert = () => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
setShowPasswordAlert(true);
|
||||
setChangePassword(true);
|
||||
handleClose();
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (password) {
|
||||
if (!validPassword) {
|
||||
@@ -70,6 +48,22 @@ export function PasswordSecurity() {
|
||||
}
|
||||
}, [password, validPassword]);
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handleShowAlert = () => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
setShowPasswordAlert(true);
|
||||
setChangePassword(true);
|
||||
handleClose();
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', py: 2, height: 84 }}>
|
||||
@@ -80,7 +74,6 @@ export function PasswordSecurity() {
|
||||
title={t('securityForm.password')}
|
||||
subtitle={t('securityForm.determinePassword')}
|
||||
action={
|
||||
changePassword ? (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
@@ -92,23 +85,10 @@ export function PasswordSecurity() {
|
||||
textTransform: 'none',
|
||||
}}
|
||||
>
|
||||
{t('securityForm.changePassword')}
|
||||
{changePassword
|
||||
? t('securityForm.changePassword')
|
||||
: t('securityForm.addPassword')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleOpen}
|
||||
sx={{
|
||||
mt: { xs: 2, sm: 0 },
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'background.paper',
|
||||
width: { xs: '100%', sm: '142px' },
|
||||
textTransform: 'none',
|
||||
}}
|
||||
>
|
||||
{t('securityForm.addPassword')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box
|
||||
@@ -122,7 +102,6 @@ export function PasswordSecurity() {
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ height: 'auto', py: { xs: 3, sm: 4 } }}>
|
||||
{changePassword ? (
|
||||
<Box sx={{ flexDirection: 'column', px: 2, py: 2 }}>
|
||||
<Typography variant="h6">
|
||||
@@ -141,179 +120,30 @@ export function PasswordSecurity() {
|
||||
{t('securityForm.notDeterminedPassword')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Dialog
|
||||
|
||||
{/* New PasswordDialog component usage */}
|
||||
<PasswordDialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
maxWidth="xs"
|
||||
scroll="body"
|
||||
PaperProps={{ sx: { mx: 1 } }}
|
||||
>
|
||||
<DialogTitle sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseCircle size={24} color="#82B1FF" />
|
||||
</IconButton>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
{t('securityForm.addPassword')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
{changePassword ? (
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={t('securityForm.currentPassword')}
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 2 },
|
||||
mt: 2,
|
||||
}}
|
||||
handleClose={handleClose}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
currentPassword={currentPassword}
|
||||
setCurrentPassword={setCurrentPassword}
|
||||
showValidation={showValidation}
|
||||
hasNumber={hasNumber}
|
||||
hasMinLength={hasMinLength}
|
||||
hasUpperAndLower={hasUpperAndLower}
|
||||
hasSpecialChar={hasSpecialChar}
|
||||
matchPassword={matchPassword}
|
||||
loading={loading}
|
||||
handleShowAlert={handleShowAlert}
|
||||
changePassword={changePassword}
|
||||
t={t}
|
||||
/>
|
||||
<Link sx={{ fontSize: '15px' }}>
|
||||
{t('securityForm.forgetPassword')}
|
||||
</Link>
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
{password && showValidation && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '364px', width: '100%' }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('securityForm.hasNumber')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('securityForm.hasMinLength')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('securityForm.hasUpperAndLower')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('securityForm.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<TextField
|
||||
label={t('securityForm.confirmPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={confirmPassword.length > 0 && !matchPassword}
|
||||
helperText={
|
||||
confirmPassword.length > 0 && !matchPassword
|
||||
? t('securityForm.notCompatibility')
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
</DialogContent>
|
||||
) : (
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': { borderRadius: 2 },
|
||||
mt: 2,
|
||||
}}
|
||||
/>
|
||||
{password && showValidation && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '364px', width: '100%' }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('securityForm.hasNumber')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('securityForm.hasMinLength')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('securityForm.hasUpperAndLower')}
|
||||
/>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('securityForm.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<TextField
|
||||
label={t('securityForm.confirmPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
error={confirmPassword.length > 0 && !matchPassword}
|
||||
helperText={
|
||||
confirmPassword.length > 0 && !matchPassword
|
||||
? t('securityForm.notCompatibility')
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
</DialogContent>
|
||||
)}
|
||||
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{ maxWidth: '364px', height: 48, textTransform: 'none' }}
|
||||
variant="contained"
|
||||
onClick={handleShowAlert}
|
||||
disabled={!password || password !== confirmPassword || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
animation: 'spin 1s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Refresh size="20" color="#fff" />
|
||||
</Box>
|
||||
) : (
|
||||
t('securityForm.confirm')
|
||||
)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Toast
|
||||
color="success"
|
||||
open={showPasswordAlert}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Box, Typography, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import React from 'react';
|
||||
|
||||
export function RecentLogins() {
|
||||
const { t } = useTranslation('security');
|
||||
@@ -46,7 +47,7 @@ export function RecentLogins() {
|
||||
>
|
||||
<Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
|
||||
{data.map((d) => (
|
||||
<>
|
||||
<React.Fragment key={d.id}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -54,7 +55,6 @@ export function RecentLogins() {
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
minHeight: 50,
|
||||
}}
|
||||
key={d.id}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
@@ -117,7 +117,7 @@ export function RecentLogins() {
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { PasswordSecurity } from './PasswordSecurity';
|
||||
import { RecentLogins } from './RecentLogins';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
export function Security() {
|
||||
return (
|
||||
<Box sx={{ backgroundColor: 'background.paper', height: '730px' }}>
|
||||
<PasswordSecurity />
|
||||
<RecentLogins />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import { Box, Button, useTheme, useMediaQuery } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { ProfileImage } from './personlInformation/ProfileImage';
|
||||
@@ -13,6 +13,8 @@ export function PersonalInformation() {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [gender, setGender] = useState('');
|
||||
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
||||
const theme = useTheme();
|
||||
const isMdUp = useMediaQuery(theme.breakpoints.up('lg'));
|
||||
|
||||
const initialData = {
|
||||
firstName: 'محمد حسین',
|
||||
@@ -30,17 +32,28 @@ export function PersonalInformation() {
|
||||
if (isEditing) {
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
gender: gender === 'male' ? 'مرد' : gender === 'female' ? 'زن' : '',
|
||||
gender:
|
||||
gender === 'male'
|
||||
? t('settingForm.man')
|
||||
: gender === 'female'
|
||||
? t('settingForm.woman')
|
||||
: '',
|
||||
}));
|
||||
} else {
|
||||
setGender(
|
||||
data.gender === 'مرد' ? 'male' : data.gender === 'زن' ? 'female' : '',
|
||||
data.gender === t('settingForm.man')
|
||||
? 'male'
|
||||
: data.gender === t('settingForm.woman')
|
||||
? 'female'
|
||||
: '',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
{isMdUp && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -51,7 +64,11 @@ export function PersonalInformation() {
|
||||
>
|
||||
<Logo />
|
||||
</Box>
|
||||
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
|
||||
<Box
|
||||
sx={{ width: '100%', height: 1, bgcolor: 'background.default' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CardContainer
|
||||
title={t('settingForm.titlePersonalInfo')}
|
||||
subtitle={t('settingForm.descriptionPersonalInfo')}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useState, type ChangeEvent } from 'react';
|
||||
import { Box, Typography, Button, TextField, IconButton } from '@mui/material';
|
||||
import { Edit, Refresh, TickCircle } from 'iconsax-react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import PhoneDisplay from './phoneNumber/PhoneDisplay';
|
||||
import PhoneEditForm from './phoneNumber/PhoneEditForm';
|
||||
import PhoneActionButtons from './phoneNumber/PhoneActionButtons';
|
||||
|
||||
export function PhoneNumber() {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
@@ -18,10 +18,32 @@ export function PhoneNumber() {
|
||||
);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
|
||||
const [phones, setPhone] = useState([
|
||||
{ phone: '09123456789', time: '۱ ماه پیش', withCode: '+989123456789' },
|
||||
]);
|
||||
const [countryCode, setCountryCode] = useState('+98');
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [error, setError] = useState<string>();
|
||||
const [touched, setTouched] = useState<boolean>(false);
|
||||
const inputError: boolean = touched && !!error;
|
||||
|
||||
const isPhoneValid = (code: string, phone: string) => {
|
||||
const phoneNum = parsePhoneNumberFromString(code + phone);
|
||||
return phoneNum && phoneNum.isValid();
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
if (!phoneNumber) {
|
||||
setError(t('settingForm.thisFieldIsRequired'));
|
||||
}
|
||||
if (!isPhoneValid(countryCode, phoneNumber)) {
|
||||
setError(t('settingForm.phoneNumberIsInvalid'));
|
||||
} else {
|
||||
setError(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleEdit = () => {
|
||||
setIsEditing((prev) => {
|
||||
@@ -37,16 +59,6 @@ export function PhoneNumber() {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePhoneNumberChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setPhoneNumber(value);
|
||||
};
|
||||
|
||||
const handleVerificationCodeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setVerificationCode(value);
|
||||
};
|
||||
|
||||
const handleSendCode = () => {
|
||||
if (!phoneNumber) return;
|
||||
setButtonState('counting');
|
||||
@@ -79,209 +91,39 @@ export function PhoneNumber() {
|
||||
subtitle={t('settingForm.descriptionPhoneNumber')}
|
||||
highlighted={isEditing}
|
||||
action={
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: '43px',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '4px',
|
||||
bgcolor: isEditing ? 'primary.main' : 'background.paper',
|
||||
color: isEditing ? 'primary.contrastText' : 'primary.main',
|
||||
width: { xs: '100%', sm: isEditing ? '85px' : '161px' },
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{isEditing
|
||||
? t('settingForm.saveButton')
|
||||
: t('settingForm.editPhoneNumber')}
|
||||
</Button>
|
||||
</Box>
|
||||
<PhoneActionButtons
|
||||
isEditing={isEditing}
|
||||
toggleEdit={toggleEdit}
|
||||
t={t}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isEditing ? (
|
||||
<Box sx={{ px: { xs: 2, sm: 4 }, bgcolor: 'background.paper' }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6">
|
||||
{t('settingForm.editPhoneNumber')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.phoneNumberText')}(
|
||||
{phones.map((p) => p.withCode)}){t('settingForm.verb')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
name="phoneNumber"
|
||||
label={t('settingForm.newPhoneNumber')}
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
onChange={handlePhoneNumberChange}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
placeholder="09123456789"
|
||||
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
buttonState === 'counting' ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setPhoneNumber('');
|
||||
}}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<Edit size="24" color="#64B5F6" />
|
||||
</IconButton>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
|
||||
{isVerified ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TickCircle
|
||||
size="24"
|
||||
style={{ color: '#43A047' }}
|
||||
variant="Bold"
|
||||
/>
|
||||
<Typography sx={{ color: 'success.main' }}>
|
||||
{t('settingForm.successButton')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleSendCode}
|
||||
disabled={
|
||||
buttonState === 'counting' || phoneNumber.length === 0
|
||||
}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: '100%', sm: 170 },
|
||||
color: 'primary.main',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{buttonState === 'counting' ? (
|
||||
<CountDownTimer
|
||||
initialSeconds={60}
|
||||
onComplete={() => setButtonState('default')}
|
||||
<PhoneEditForm
|
||||
phoneNumber={phoneNumber}
|
||||
setPhoneNumber={setPhoneNumber}
|
||||
countryCode={countryCode}
|
||||
setCountryCode={setCountryCode}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
isVerified={isVerified}
|
||||
isVerifying={isVerifying}
|
||||
buttonState={buttonState}
|
||||
setButtonState={setButtonState}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyClick={handleVerifyClick}
|
||||
error={error}
|
||||
inputError={inputError}
|
||||
handleBlur={handleBlur}
|
||||
textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>}
|
||||
inputRef={inputRef as React.RefObject<HTMLInputElement>}
|
||||
phones={phones}
|
||||
showToast={showToast}
|
||||
setShowToast={setShowToast}
|
||||
t={t}
|
||||
/>
|
||||
) : (
|
||||
t('settingForm.verificationCodeButton')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{buttonState === 'counting' && !isVerified && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
name="verificationCode"
|
||||
label={t('settingForm.verificationCode')}
|
||||
type="tel"
|
||||
value={verificationCode}
|
||||
onChange={handleVerificationCodeChange}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
placeholder={t('settingForm.verificationCode')}
|
||||
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleVerifyClick}
|
||||
disabled={isVerifying || verificationCode.length === 0}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: '100%', sm: 170 },
|
||||
bgcolor: 'primary.main',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{isVerifying ? (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
animation: 'spin 1s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Refresh size="20" color="white" />
|
||||
</Box>
|
||||
) : (
|
||||
t('settingForm.checkCode')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Toast
|
||||
color="success"
|
||||
open={showToast}
|
||||
onClose={() => setShowToast(false)}
|
||||
>
|
||||
{t('settingForm.successfulChangePhone')}
|
||||
</Toast>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ px: { xs: 2, sm: 4 } }}>
|
||||
{phones.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
py: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.primary">
|
||||
{item.phone}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<PhoneDisplay phones={phones} />
|
||||
)}
|
||||
</CardContainer>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
IconButton,
|
||||
TextField,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import type { TransitionProps } from '@mui/material/transitions';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
|
||||
import {
|
||||
Google,
|
||||
Apple,
|
||||
Sms,
|
||||
Trash,
|
||||
CloseSquare,
|
||||
Message,
|
||||
ArrowDown3,
|
||||
} from 'iconsax-react';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
|
||||
const MobileSlide = React.forwardRef(function MobileSlide(
|
||||
props: TransitionProps & { children: React.ReactElement<any, any> },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
import SocialMediaList from './socialMedia/SocialMediaList';
|
||||
import SocialMediaMenu from './socialMedia/SocialMediaMenu';
|
||||
import SocialMediaDialog from './socialMedia/SocialMediaDialog';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
|
||||
export function SocialMedia() {
|
||||
const { t } = useTranslation('profileSetting');
|
||||
@@ -44,9 +14,7 @@ export function SocialMedia() {
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [emailInput, setEmailInput] = useState('');
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const openMenu = Boolean(anchor);
|
||||
const fullScreen = useMediaQuery((theme: Theme) =>
|
||||
theme.breakpoints.down('sm'),
|
||||
);
|
||||
@@ -58,19 +26,6 @@ export function SocialMedia() {
|
||||
| 'lg'
|
||||
| 'xl';
|
||||
|
||||
const handleOpenDialog = () => setOpenDialog(true);
|
||||
const handleCloseDialog = () => setOpenDialog(false);
|
||||
|
||||
const handleClickMenu = (e: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setAnchor(e.currentTarget);
|
||||
const handleCloseMenu = () => setAnchor(null);
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmailInput(value);
|
||||
setEmailError(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
|
||||
};
|
||||
|
||||
const emailList = [
|
||||
{ email: 'emailtemp@email.com', provider: 'email', time: '1 ماه پیش' },
|
||||
{ email: 'emailtemp@gmail.com', provider: 'google', time: '1 ماه پیش' },
|
||||
@@ -83,286 +38,21 @@ export function SocialMedia() {
|
||||
title={t('settingForm.titleSocial')}
|
||||
subtitle={t('settingForm.descriptionSocial')}
|
||||
action={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'stretch', sm: 'flex-start' },
|
||||
gap: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClickMenu}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: { sm: 187 },
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '8px',
|
||||
color: 'primary.main',
|
||||
fontSize: '14px',
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
direction: 'rtl',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.addEmailOrSocialButton')}
|
||||
</Box>
|
||||
<ArrowDown3 size="20" color="#2979FF" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={openMenu}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minWidth: 240,
|
||||
maxWidth: '90vw',
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
handleOpenDialog();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Message size={20} color="black" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.email')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Google size={20} color="#4285F4" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.google')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Apple size={20} color="black" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.apple')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
<SocialMediaMenu t={t} onOpenDialog={() => setOpenDialog(true)} />
|
||||
}
|
||||
>
|
||||
<Box sx={{ width: '100%', borderRadius: '8px', p: { xs: 1, sm: 2 } }}>
|
||||
{emailList.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
py: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{item.provider === 'google' && (
|
||||
<Google size="20" variant="Bold" color="#4285F4" />
|
||||
)}
|
||||
{item.provider === 'apple' && (
|
||||
<Apple size="20" variant="Bold" color="black" />
|
||||
)}
|
||||
{item.provider === 'email' && (
|
||||
<Sms size="20" variant="Bold" color="#1976d2" />
|
||||
)}
|
||||
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="h6" noWrap>
|
||||
{item.email}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton size="small" aria-label="Delete">
|
||||
<Trash size="20" color="gray" variant="Outline" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Dialog
|
||||
<SocialMediaList t={t} emailList={emailList} />
|
||||
<SocialMediaDialog
|
||||
open={openDialog}
|
||||
onClose={handleCloseDialog}
|
||||
fullWidth
|
||||
onClose={() => setOpenDialog(false)}
|
||||
t={t}
|
||||
emailInput={emailInput}
|
||||
setEmailInput={setEmailInput}
|
||||
emailError={emailError}
|
||||
setEmailError={setEmailError}
|
||||
fullScreen={fullScreen}
|
||||
maxWidth={computedMaxWidth}
|
||||
scroll="paper"
|
||||
keepMounted
|
||||
TransitionComponent={fullScreen ? MobileSlide : undefined}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
m: { xs: 0, sm: 2 },
|
||||
borderRadius: { xs: 0, sm: 2 },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
height: { xs: '100%', sm: 'auto' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
gap: 1,
|
||||
p: { xs: 1.5, sm: 2 },
|
||||
position: { xs: 'sticky', sm: 'static' },
|
||||
top: 0,
|
||||
bgcolor: 'background.paper',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={handleCloseDialog} aria-label="Close">
|
||||
<CloseSquare size={24} color="#82B1FF" />
|
||||
</IconButton>
|
||||
{t('settingForm.addEmailButton')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
dividers
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
py: { xs: 1.5, sm: 2 },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography fontWeight="bold">
|
||||
{t('settingForm.newEmail')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.dialogHeader')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={handleEmailChange}
|
||||
error={emailError}
|
||||
helperText={emailError ? t('settingForm.emailError') : ''}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
autoFocus
|
||||
computedMaxWidth={computedMaxWidth}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{ textTransform: 'none', borderRadius: 2 }}
|
||||
disabled={emailError || emailInput === ''}
|
||||
>
|
||||
{t('settingForm.verificationCodeButton')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
|
||||
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
|
||||
<Typography sx={{ mx: 1, fontSize: 12, color: 'text.secondary' }}>
|
||||
{t('settingForm.or')}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: 2,
|
||||
borderColor: 'primary.main',
|
||||
color: 'primary.main',
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Google
|
||||
size="20"
|
||||
color="#4285F4"
|
||||
style={{ marginInlineStart: 8 }}
|
||||
/>
|
||||
{t('settingForm.google')}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: 2,
|
||||
borderColor: 'primary.main',
|
||||
color: 'primary.main',
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Apple
|
||||
size="20"
|
||||
color="black"
|
||||
style={{ marginInlineStart: 8 }}
|
||||
/>
|
||||
{t('settingForm.apple')}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContainer>
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Box, Button } from '@mui/material';
|
||||
|
||||
export default function PhoneActionButtons({
|
||||
isEditing,
|
||||
toggleEdit,
|
||||
t,
|
||||
}: {
|
||||
isEditing: boolean;
|
||||
toggleEdit: () => void;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: '43px',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.rejectButton')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '4px',
|
||||
bgcolor: isEditing ? 'primary.main' : 'background.paper',
|
||||
color: isEditing ? 'primary.contrastText' : 'primary.main',
|
||||
width: { xs: '100%', sm: isEditing ? '85px' : '161px' },
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{isEditing
|
||||
? t('settingForm.saveButton')
|
||||
: t('settingForm.editPhoneNumber')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export default function PhoneDisplay({
|
||||
phones,
|
||||
}: {
|
||||
phones: { phone: string; time: string }[];
|
||||
}) {
|
||||
return (
|
||||
<Box sx={{ px: { xs: 2, sm: 4 } }}>
|
||||
{phones.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
py: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.primary">
|
||||
{item.phone}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Box, Typography, TextField, IconButton, Button } from '@mui/material';
|
||||
import { Edit, Refresh, TickCircle } from 'iconsax-react';
|
||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { CountryCodeSelector } from '../../CountryCodeSelector';
|
||||
|
||||
export default function PhoneEditForm({
|
||||
phoneNumber,
|
||||
setPhoneNumber,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
isVerified,
|
||||
isVerifying,
|
||||
buttonState,
|
||||
setButtonState,
|
||||
handleSendCode,
|
||||
handleVerifyClick,
|
||||
error,
|
||||
inputError,
|
||||
handleBlur,
|
||||
textFieldRef,
|
||||
inputRef,
|
||||
phones,
|
||||
showToast,
|
||||
setShowToast,
|
||||
t,
|
||||
}: {
|
||||
phoneNumber: string;
|
||||
setPhoneNumber: (v: string) => void;
|
||||
countryCode: string;
|
||||
setCountryCode: (v: string) => void;
|
||||
verificationCode: string;
|
||||
setVerificationCode: (v: string) => void;
|
||||
isVerified: boolean;
|
||||
isVerifying: boolean;
|
||||
buttonState: 'default' | 'counting';
|
||||
setButtonState: (v: 'default' | 'counting') => void;
|
||||
handleSendCode: () => void;
|
||||
handleVerifyClick: () => void;
|
||||
error?: string;
|
||||
inputError: boolean;
|
||||
handleBlur: () => void;
|
||||
textFieldRef: React.RefObject<HTMLDivElement>;
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
phones: { phone: string; time: string; withCode: string }[];
|
||||
showToast: boolean;
|
||||
setShowToast: (v: boolean) => void;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
return (
|
||||
<Box sx={{ px: { xs: 2, sm: 4 }, bgcolor: 'background.paper' }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6">{t('settingForm.editPhoneNumber')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.phoneNumberText')}({phones.map((p) => p.withCode)})
|
||||
{t('settingForm.verb')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}
|
||||
>
|
||||
<TextField
|
||||
name="phoneNumber"
|
||||
label={t('settingForm.newPhoneNumber')}
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
placeholder="09123456789"
|
||||
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
buttonState === 'counting' ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setButtonState('default');
|
||||
setPhoneNumber('');
|
||||
}}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<Edit size="24" color="#64B5F6" />
|
||||
</IconButton>
|
||||
) : null,
|
||||
}}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
dir: 'auto',
|
||||
sx: { lineHeight: 1.5, paddingX: 0 },
|
||||
},
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{isVerified ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TickCircle size="24" style={{ color: '#43A047' }} variant="Bold" />
|
||||
<Typography sx={{ color: 'success.main' }}>
|
||||
{t('settingForm.successButton')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleSendCode}
|
||||
disabled={buttonState === 'counting' || phoneNumber.length === 0}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: '100%', sm: 170 },
|
||||
color: 'primary.main',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{buttonState === 'counting' ? (
|
||||
<CountDownTimer
|
||||
initialSeconds={60}
|
||||
onComplete={() => setButtonState('default')}
|
||||
/>
|
||||
) : (
|
||||
t('settingForm.verificationCodeButton')
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{buttonState === 'counting' && !isVerified && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
name="verificationCode"
|
||||
label={t('settingForm.verificationCode')}
|
||||
type="tel"
|
||||
value={verificationCode}
|
||||
onChange={(e) =>
|
||||
setVerificationCode(e.target.value.replace(/\D/g, ''))
|
||||
}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
placeholder={t('settingForm.verificationCode')}
|
||||
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleVerifyClick}
|
||||
disabled={isVerifying || verificationCode.length === 0}
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
minWidth: { xs: '100%', sm: 170 },
|
||||
bgcolor: 'primary.main',
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{isVerifying ? (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
animation: 'spin 1s linear infinite',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Refresh size="20" color="white" />
|
||||
</Box>
|
||||
) : (
|
||||
t('settingForm.checkCode')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Toast
|
||||
color="success"
|
||||
open={showToast}
|
||||
onClose={() => setShowToast(false)}
|
||||
>
|
||||
{t('settingForm.successfulChangePhone')}
|
||||
</Toast>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import type { TransitionProps } from '@mui/material/transitions';
|
||||
import { CloseSquare, Google, Apple } from 'iconsax-react';
|
||||
|
||||
const MobileSlide = React.forwardRef(function MobileSlide(
|
||||
props: TransitionProps & { children: React.ReactElement<any, any> },
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export default function SocialMediaDialog({
|
||||
open,
|
||||
onClose,
|
||||
t,
|
||||
emailInput,
|
||||
setEmailInput,
|
||||
emailError,
|
||||
setEmailError,
|
||||
fullScreen,
|
||||
computedMaxWidth,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
t: (key: string) => string;
|
||||
emailInput: string;
|
||||
setEmailInput: (val: string) => void;
|
||||
emailError: boolean;
|
||||
setEmailError: (val: boolean) => void;
|
||||
fullScreen: boolean;
|
||||
computedMaxWidth: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
}) {
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setEmailInput(value);
|
||||
setEmailError(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
maxWidth={computedMaxWidth}
|
||||
scroll="paper"
|
||||
keepMounted
|
||||
TransitionComponent={fullScreen ? MobileSlide : undefined}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
m: { xs: 0, sm: 2 },
|
||||
borderRadius: { xs: 0, sm: 2 },
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
height: { xs: '100%', sm: 'auto' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
gap: 1,
|
||||
p: { xs: 1.5, sm: 2 },
|
||||
position: { xs: 'sticky', sm: 'static' },
|
||||
top: 0,
|
||||
bgcolor: 'background.paper',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={onClose} aria-label="Close">
|
||||
<CloseSquare size={24} color="#82B1FF" />
|
||||
</IconButton>
|
||||
{t('settingForm.addEmailButton')}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
dividers
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
py: { xs: 1.5, sm: 2 },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography fontWeight="bold">{t('settingForm.newEmail')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.dialogHeader')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={handleEmailChange}
|
||||
error={emailError}
|
||||
helperText={emailError ? t('settingForm.emailError') : ''}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{ textTransform: 'none', borderRadius: 2 }}
|
||||
disabled={emailError || emailInput === ''}
|
||||
>
|
||||
{t('settingForm.verificationCodeButton')}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
|
||||
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
|
||||
<Typography sx={{ mx: 1, fontSize: 12, color: 'text.secondary' }}>
|
||||
{t('settingForm.or')}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: 2,
|
||||
borderColor: 'primary.main',
|
||||
color: 'primary.main',
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Google
|
||||
size="20"
|
||||
color="#4285F4"
|
||||
style={{ marginInlineStart: 8 }}
|
||||
/>
|
||||
{t('settingForm.google')}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
sx={{
|
||||
textTransform: 'none',
|
||||
border: 2,
|
||||
borderColor: 'primary.main',
|
||||
color: 'primary.main',
|
||||
borderRadius: 2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Apple size="20" color="black" style={{ marginInlineStart: 8 }} />
|
||||
{t('settingForm.apple')}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Box, Typography, IconButton } from '@mui/material';
|
||||
import { Google, Apple, Sms, Trash } from 'iconsax-react';
|
||||
|
||||
export default function SocialMediaList({
|
||||
emailList,
|
||||
}: {
|
||||
t: (key: string) => string;
|
||||
emailList: readonly {
|
||||
email: string;
|
||||
provider: 'email' | 'google' | 'apple';
|
||||
time: string;
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<Box sx={{ width: '100%', borderRadius: '8px', p: { xs: 1, sm: 2 } }}>
|
||||
{emailList.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
py: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{item.provider === 'google' && (
|
||||
<Google size="20" variant="Bold" color="#4285F4" />
|
||||
)}
|
||||
{item.provider === 'apple' && (
|
||||
<Apple size="20" variant="Bold" color="black" />
|
||||
)}
|
||||
{item.provider === 'email' && (
|
||||
<Sms size="20" variant="Bold" color="#1976d2" />
|
||||
)}
|
||||
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography variant="h6" noWrap>
|
||||
{item.email}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton size="small" aria-label="Delete">
|
||||
<Trash size="20" color="gray" variant="Outline" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Message, Google, Apple, ArrowDown3 } from 'iconsax-react';
|
||||
|
||||
export default function SocialMediaMenu({
|
||||
t,
|
||||
onOpenDialog,
|
||||
}: {
|
||||
t: (key: string) => string;
|
||||
onOpenDialog: () => void;
|
||||
}) {
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
|
||||
const openMenu = Boolean(anchor);
|
||||
|
||||
const handleClickMenu = (e: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setAnchor(e.currentTarget);
|
||||
const handleCloseMenu = () => setAnchor(null);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
alignItems: { xs: 'stretch', sm: 'flex-start' },
|
||||
gap: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={handleClickMenu}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: { sm: 187 },
|
||||
textTransform: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.main',
|
||||
borderRadius: '8px',
|
||||
color: 'primary.main',
|
||||
fontSize: '14px',
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
direction: 'rtl',
|
||||
}}
|
||||
>
|
||||
{t('settingForm.addEmailOrSocialButton')}
|
||||
</Box>
|
||||
<ArrowDown3 size="20" color="#2979FF" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={openMenu}
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minWidth: 240,
|
||||
maxWidth: '90vw',
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
onOpenDialog();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Message size={20} color="black" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.email')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Google size={20} color="#4285F4" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.google')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItemIcon>
|
||||
<Apple size={20} color="black" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('settingForm.apple')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
271
src/features/profile/data/countries.ts
Normal file
271
src/features/profile/data/countries.ts
Normal file
@@ -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' },
|
||||
];
|
||||
@@ -1,312 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
RouterProvider,
|
||||
Navigate,
|
||||
Outlet,
|
||||
useLocation,
|
||||
} from 'react-router-dom';
|
||||
import { SideNav, type NavItemConfig } from '@rkheftan/harmony-ui';
|
||||
import {
|
||||
Devices,
|
||||
LocationTick,
|
||||
Mobile,
|
||||
PasswordCheck,
|
||||
Personalcard,
|
||||
ProfileCircle,
|
||||
Setting as SettingIcon,
|
||||
Shield,
|
||||
Sms,
|
||||
Menu,
|
||||
} from 'iconsax-react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
Drawer,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ActiveDevices } from '../components/activeDevices/ActiveDevices';
|
||||
import { Setting } from '../components/setting/Setting';
|
||||
import { RecentLogins } from '../components/security/RecentLogins';
|
||||
import { PasswordSecurity } from '../components/security/PasswordSecurity';
|
||||
import { PersonalInformation } from '../components/userInformation/PersonalInformation';
|
||||
import { PhoneNumber } from '../components/userInformation/PhoneNumber';
|
||||
import { SocialMedia } from '../components/userInformation/SocialMedia';
|
||||
|
||||
function Header() {
|
||||
const headers = [{ name: 'محمدحسین برزه گر', phone: '09123456789' }];
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: 84,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
{headers.map((h) => (
|
||||
<Box key={h.phone}>
|
||||
<Typography variant="body2">{h.name}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{h.phone}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
const theme = useTheme();
|
||||
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
|
||||
const location = useLocation();
|
||||
const { t } = useTranslation('sideMap');
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const navWidthMd = '274px';
|
||||
|
||||
const navConfig: NavItemConfig[] = [
|
||||
{
|
||||
text: t('side.account'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<ProfileCircle size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<ProfileCircle size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile',
|
||||
children: [
|
||||
{
|
||||
text: t('side.personalInfo'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<Personalcard size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Personalcard size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile#info',
|
||||
},
|
||||
{
|
||||
text: t('side.phoneNumber'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<Mobile size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Mobile size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile#contact-info',
|
||||
},
|
||||
{
|
||||
text: t('side.email'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<Sms size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Sms size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/profile#email',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: t('side.security'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<Shield size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Shield size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security',
|
||||
children: [
|
||||
{
|
||||
text: t('side.password'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<PasswordCheck size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<PasswordCheck size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security#password',
|
||||
},
|
||||
{
|
||||
text: t('side.verifiedAddress'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<LocationTick size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<LocationTick size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security#locations',
|
||||
},
|
||||
{
|
||||
text: t('side.recentLogins'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<Devices size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Devices size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/security#sessions',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: t('side.activeDevices'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<Devices size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<Devices size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/devices',
|
||||
},
|
||||
{
|
||||
text: t('side.settings'),
|
||||
getIcon: (sel) =>
|
||||
sel ? (
|
||||
<SettingIcon size={24} color="#1976d2" variant="Bold" />
|
||||
) : (
|
||||
<SettingIcon size={24} color="#82B1FF" />
|
||||
),
|
||||
path: '/setting',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
px: { xs: 0, sm: 0, md: '185px' },
|
||||
py: { xs: 0, sm: 0, md: '105px' },
|
||||
boxSizing: 'border-box',
|
||||
backgroundColor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width={{ xs: '100vw', md: 1100 }}
|
||||
height="100vh"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
bgcolor="background.paper"
|
||||
overflow="hidden"
|
||||
borderRadius={{ xs: 0, md: 2 }}
|
||||
boxShadow={{ xs: 0, md: 3 }}
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{!isMdUp && (
|
||||
<AppBar position="static" elevation={0}>
|
||||
<Toolbar sx={{ backgroundColor: 'background.paper' }}>
|
||||
<IconButton edge="start" onClick={() => setDrawerOpen(true)}>
|
||||
<Menu size={24} color="#1976d2" variant="Bold" />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)}
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
{isMdUp && (
|
||||
<Box
|
||||
sx={{
|
||||
width: navWidthMd,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<SideNav
|
||||
navConfig={navConfig}
|
||||
header={<Header />}
|
||||
activePath={location.pathname + location.hash}
|
||||
sideNavVariant="full"
|
||||
positioning="absolute"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
px: { xs: 2, sm: 3 },
|
||||
py: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{!isMdUp && (
|
||||
<Drawer
|
||||
anchor="left"
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
>
|
||||
<Box width={navWidthMd}>
|
||||
<SideNav
|
||||
navConfig={navConfig}
|
||||
header={<Header />}
|
||||
activePath={location.pathname + location.hash}
|
||||
sideNavVariant="full"
|
||||
/>
|
||||
</Box>
|
||||
</Drawer>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{ path: '/', element: <Navigate to="/profile" replace /> },
|
||||
{
|
||||
path: '/profile',
|
||||
element: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<div id="info">
|
||||
<PersonalInformation />
|
||||
</div>
|
||||
<div id="contact-info">
|
||||
<PhoneNumber />
|
||||
</div>
|
||||
<div id="email">
|
||||
<SocialMedia />
|
||||
</div>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/security',
|
||||
element: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<div id="password">
|
||||
<PasswordSecurity />
|
||||
</div>
|
||||
<div id="locations"></div>
|
||||
<div id="sessions">
|
||||
<RecentLogins />
|
||||
</div>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{ path: '/devices', element: <ActiveDevices /> },
|
||||
{ path: '/setting', element: <Setting /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from '../components/layout/Routes';
|
||||
|
||||
export function Settings() {
|
||||
return <RouterProvider router={router} />;
|
||||
|
||||
Reference in New Issue
Block a user