Merge pull request #12 from rkheftan/feat/login-signup
Feat/login signup
This commit is contained in:
1
.env
Normal file
1
.env
Normal file
@@ -0,0 +1 @@
|
||||
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
|
||||
57
index.html
57
index.html
@@ -1,29 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Harmony club</title>
|
||||
<!-- this script add for preventing initial theme flashing -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
const THEME_STORAGE_KEY = 'mui-mode';
|
||||
const DARK_THEME_CLASS_NAME = 'dark';
|
||||
const savedMode = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
const prefersDark = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
).matches;
|
||||
if (savedMode === 'dark' || (!savedMode && prefersDark)) {
|
||||
document.documentElement.classList.add(DARK_THEME_CLASS_NAME);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Harmony club</title>
|
||||
<!-- this script add for preventing initial theme flashing -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
const THEME_STORAGE_KEY = 'mui-mode';
|
||||
const DARK_THEME_CLASS_NAME = 'dark';
|
||||
const savedMode = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
const prefersDark = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
).matches;
|
||||
if (savedMode === 'dark' || (!savedMode && prefersDark)) {
|
||||
document.documentElement.classList.add(DARK_THEME_CLASS_NAME);
|
||||
}
|
||||
} catch (e) { }
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -18,7 +18,9 @@
|
||||
"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",
|
||||
"react-i18next": "^15.6.0",
|
||||
"react-router-dom": "^7.8.0",
|
||||
@@ -30,6 +32,7 @@
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
@@ -2253,6 +2256,7 @@
|
||||
"version": "4.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz",
|
||||
"integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
@@ -3042,13 +3046,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@@ -4115,27 +4112,6 @@
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
@@ -4476,6 +4452,11 @@
|
||||
"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",
|
||||
@@ -5090,6 +5071,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-country-flag": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz",
|
||||
"integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
"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",
|
||||
"react-i18next": "^15.6.0",
|
||||
"react-router-dom": "^7.8.0",
|
||||
@@ -33,6 +35,7 @@
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
|
||||
53
public/locales/en/authentication.json
Normal file
53
public/locales/en/authentication.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"loginForm": {
|
||||
"title": "Login/Register",
|
||||
"description": "Please enter your email/password to start",
|
||||
"emailOrPhoneLabel": "Email/Password",
|
||||
"submitButton": "Login/Register",
|
||||
"loginWithGoogle": "Login with google",
|
||||
"emailIsInvalid": "Email is invalid",
|
||||
"phoneNumberIsInvalid": "Phone number is invalid",
|
||||
"thisFieldIsRequired": "This field is requried"
|
||||
},
|
||||
"verify": {
|
||||
"verify": "Verify",
|
||||
"a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.",
|
||||
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.",
|
||||
"a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.",
|
||||
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.",
|
||||
"theVerificationCodeIsIncorrect": "The verification code is incorrect.",
|
||||
"youHaveSuccessfullyLoggedIn": "You have successfully logged in",
|
||||
"youHaveSuccessfullySignedIn": "You have successfully signed in",
|
||||
"resendCodeIn": "Resend code in",
|
||||
"moreMinute": "minute",
|
||||
"resendCode": "Resend code"
|
||||
},
|
||||
"completeSignUp": {
|
||||
"completeSignUp": "Complete Sign Up",
|
||||
"emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "Email {{ email }} has been successfully verified. Please enter your contact number to continue.",
|
||||
"phoneNumber": "Phone number"
|
||||
},
|
||||
"enterPassword": {
|
||||
"loginWithPassword": "Login with password",
|
||||
"enterThePasswordYouSetForYourAccount": "Enter the password you set for your account.",
|
||||
"loginPassword": "Login password",
|
||||
"loginWithOneTimeCode": "Login with one-time code",
|
||||
"iForgotMyPassword": "I forgot my password."
|
||||
},
|
||||
"forgetPassword": {
|
||||
"forgetPassword": "Forget password",
|
||||
"pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "Please enter your mobile number/email to recover your password.",
|
||||
"anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "An email containing a recovery code has been sent to this email address.",
|
||||
"anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "An recovery code has been sent to this phone number.",
|
||||
"confirm": "Confirm",
|
||||
"changePassword": "Change password",
|
||||
"createANewPassword": "Create a new password",
|
||||
"newPassword": "New password",
|
||||
"includingANumber": "Including a number",
|
||||
"atLeast8Characters": "At least 8 characters",
|
||||
"containsAnUppercaseAndLowercaseLetter": "Contains an uppercase and lowercase letter",
|
||||
"ContainsASymbol": "Contains the symbol (!@#$%&*^)",
|
||||
"confirmPassword": "Confirm password",
|
||||
"passwordChangedSuccessfully": "Password changed successfully"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,250 @@
|
||||
{
|
||||
"helloWorld": "hello world"
|
||||
"helloWorld": "hello world",
|
||||
"country": {
|
||||
"afghanistan": "Afghanistan",
|
||||
"aland_islands": "Aland islands",
|
||||
"albania": "Albania",
|
||||
"algeria": "Algeria",
|
||||
"american_samoa": "American samoa",
|
||||
"andorra": "Andorra",
|
||||
"angola": "Angola",
|
||||
"anguilla": "Anguilla",
|
||||
"antarctica": "Antarctica",
|
||||
"antigua_and_barbuda": "Antigua and barbuda",
|
||||
"argentina": "Argentina",
|
||||
"armenia": "Armenia",
|
||||
"aruba": "Aruba",
|
||||
"australia": "Australia",
|
||||
"austria": "Austria",
|
||||
"azerbaijan": "Azerbaijan",
|
||||
"bahamas": "Bahamas",
|
||||
"bahrain": "Bahrain",
|
||||
"bangladesh": "Bangladesh",
|
||||
"barbados": "Barbados",
|
||||
"belarus": "Belarus",
|
||||
"belgium": "Belgium",
|
||||
"belize": "Belize",
|
||||
"benin": "Benin",
|
||||
"bermuda": "Bermuda",
|
||||
"bhutan": "Bhutan",
|
||||
"bolivia": "Bolivia",
|
||||
"bosnia_and_herzegovina": "Bosnia and herzegovina",
|
||||
"botswana": "Botswana",
|
||||
"brazil": "Brazil",
|
||||
"british_indian_ocean_territory": "British indian ocean territory",
|
||||
"british_virgin_islands": "British virgin islands",
|
||||
"brunei": "Brunei",
|
||||
"bulgaria": "Bulgaria",
|
||||
"burkina_faso": "Burkina faso",
|
||||
"burundi": "Burundi",
|
||||
"cambodia": "Cambodia",
|
||||
"cameroon": "Cameroon",
|
||||
"canada": "Canada",
|
||||
"cape_verde": "Cape verde",
|
||||
"cayman_islands": "Cayman islands",
|
||||
"central_african_republic": "Central african republic",
|
||||
"chad": "Chad",
|
||||
"chile": "Chile",
|
||||
"china": "China",
|
||||
"christmas_island": "Christmas island",
|
||||
"cocos_keeling_islands": "Cocos keeling islands",
|
||||
"colombia": "Colombia",
|
||||
"comoros": "Comoros",
|
||||
"congo_brazzaville": "Congo brazzaville",
|
||||
"congo_kinshasa": "Congo kinshasa",
|
||||
"cook_islands": "Cook islands",
|
||||
"costa_rica": "Costa rica",
|
||||
"cote_divoire": "Cote divoire",
|
||||
"croatia": "Croatia",
|
||||
"cuba": "Cuba",
|
||||
"curacao": "Curacao",
|
||||
"cyprus": "Cyprus",
|
||||
"czech_republic": "Czech republic",
|
||||
"denmark": "Denmark",
|
||||
"djibouti": "Djibouti",
|
||||
"dominica": "Dominica",
|
||||
"dominican_republic": "Dominican republic",
|
||||
"ecuador": "Ecuador",
|
||||
"egypt": "Egypt",
|
||||
"el_salvador": "El salvador",
|
||||
"equatorial_guinea": "Equatorial guinea",
|
||||
"eritrea": "Eritrea",
|
||||
"estonia": "Estonia",
|
||||
"eswatini": "Eswatini",
|
||||
"ethiopia": "Ethiopia",
|
||||
"falkland_islands": "Falkland islands",
|
||||
"faroe_islands": "Faroe islands",
|
||||
"fiji": "Fiji",
|
||||
"finland": "Finland",
|
||||
"france": "France",
|
||||
"french_guiana": "French guiana",
|
||||
"french_polynesia": "French polynesia",
|
||||
"gabon": "Gabon",
|
||||
"gambia": "Gambia",
|
||||
"georgia": "Georgia",
|
||||
"germany": "Germany",
|
||||
"ghana": "Ghana",
|
||||
"gibraltar": "Gibraltar",
|
||||
"greece": "Greece",
|
||||
"greenland": "Greenland",
|
||||
"grenada": "Grenada",
|
||||
"guadeloupe": "Guadeloupe",
|
||||
"guam": "Guam",
|
||||
"guatemala": "Guatemala",
|
||||
"guernsey": "Guernsey",
|
||||
"guinea": "Guinea",
|
||||
"guinea_bissau": "Guinea bissau",
|
||||
"guyana": "Guyana",
|
||||
"haiti": "Haiti",
|
||||
"honduras": "Honduras",
|
||||
"hong_kong": "Hong kong",
|
||||
"hungary": "Hungary",
|
||||
"iceland": "Iceland",
|
||||
"india": "India",
|
||||
"indonesia": "Indonesia",
|
||||
"iran": "Iran",
|
||||
"iraq": "Iraq",
|
||||
"ireland": "Ireland",
|
||||
"isle_of_man": "Isle of man",
|
||||
"israel": "Israel",
|
||||
"italy": "Italy",
|
||||
"jamaica": "Jamaica",
|
||||
"japan": "Japan",
|
||||
"jersey": "Jersey",
|
||||
"jordan": "Jordan",
|
||||
"kazakhstan": "Kazakhstan",
|
||||
"kenya": "Kenya",
|
||||
"kiribati": "Kiribati",
|
||||
"kosovo": "Kosovo",
|
||||
"kuwait": "Kuwait",
|
||||
"kyrgyzstan": "Kyrgyzstan",
|
||||
"laos": "Laos",
|
||||
"latvia": "Latvia",
|
||||
"lebanon": "Lebanon",
|
||||
"lesotho": "Lesotho",
|
||||
"liberia": "Liberia",
|
||||
"libya": "Libya",
|
||||
"liechtenstein": "Liechtenstein",
|
||||
"lithuania": "Lithuania",
|
||||
"luxembourg": "Luxembourg",
|
||||
"macau": "Macau",
|
||||
"madagascar": "Madagascar",
|
||||
"malawi": "Malawi",
|
||||
"malaysia": "Malaysia",
|
||||
"maldives": "Maldives",
|
||||
"mali": "Mali",
|
||||
"malta": "Malta",
|
||||
"marshall_islands": "Marshall islands",
|
||||
"martinique": "Martinique",
|
||||
"mauritania": "Mauritania",
|
||||
"mauritius": "Mauritius",
|
||||
"mayotte": "Mayotte",
|
||||
"mexico": "Mexico",
|
||||
"micronesia": "Micronesia",
|
||||
"moldova": "Moldova",
|
||||
"monaco": "Monaco",
|
||||
"mongolia": "Mongolia",
|
||||
"montenegro": "Montenegro",
|
||||
"montserrat": "Montserrat",
|
||||
"morocco": "Morocco",
|
||||
"mozambique": "Mozambique",
|
||||
"myanmar": "Myanmar",
|
||||
"namibia": "Namibia",
|
||||
"nauru": "Nauru",
|
||||
"nepal": "Nepal",
|
||||
"netherlands": "Netherlands",
|
||||
"new_caledonia": "New caledonia",
|
||||
"new_zealand": "New zealand",
|
||||
"nicaragua": "Nicaragua",
|
||||
"niger": "Niger",
|
||||
"nigeria": "Nigeria",
|
||||
"niue": "Niue",
|
||||
"norfolk_island": "Norfolk island",
|
||||
"north_korea": "North korea",
|
||||
"north_macedonia": "North macedonia",
|
||||
"northern_mariana_islands": "Northern mariana islands",
|
||||
"norway": "Norway",
|
||||
"oman": "Oman",
|
||||
"pakistan": "Pakistan",
|
||||
"palau": "Palau",
|
||||
"palestine": "Palestine",
|
||||
"panama": "Panama",
|
||||
"papua_new_guinea": "Papua new guinea",
|
||||
"paraguay": "Paraguay",
|
||||
"peru": "Peru",
|
||||
"philippines": "Philippines",
|
||||
"pitcairn_islands": "Pitcairn islands",
|
||||
"poland": "Poland",
|
||||
"portugal": "Portugal",
|
||||
"puerto_rico": "Puerto rico",
|
||||
"qatar": "Qatar",
|
||||
"reunion": "Reunion",
|
||||
"romania": "Romania",
|
||||
"russia": "Russia",
|
||||
"rwanda": "Rwanda",
|
||||
"saint_barthelemy": "Saint barthelemy",
|
||||
"saint_helena": "Saint helena",
|
||||
"saint_kitts_and_nevis": "Saint kitts and nevis",
|
||||
"saint_lucia": "Saint lucia",
|
||||
"saint_martin": "Saint martin",
|
||||
"saint_pierre_and_miquelon": "Saint pierre and miquelon",
|
||||
"saint_vincent_and_the_grenadines": "Saint vincent and the grenadines",
|
||||
"samoa": "Samoa",
|
||||
"san_marino": "San marino",
|
||||
"sao_tome_and_principe": "Sao tome and principe",
|
||||
"saudi_arabia": "Saudi arabia",
|
||||
"senegal": "Senegal",
|
||||
"serbia": "Serbia",
|
||||
"seychelles": "Seychelles",
|
||||
"sierra_leone": "Sierra leone",
|
||||
"singapore": "Singapore",
|
||||
"sint_maarten": "Sint maarten",
|
||||
"slovakia": "Slovakia",
|
||||
"slovenia": "Slovenia",
|
||||
"solomon_islands": "Solomon islands",
|
||||
"somalia": "Somalia",
|
||||
"south_africa": "South africa",
|
||||
"south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands",
|
||||
"south_korea": "South korea",
|
||||
"south_sudan": "South sudan",
|
||||
"spain": "Spain",
|
||||
"sri_lanka": "Sri lanka",
|
||||
"sudan": "Sudan",
|
||||
"suriname": "Suriname",
|
||||
"svalbard_and_jan_mayen": "Svalbard and jan mayen",
|
||||
"sweden": "Sweden",
|
||||
"switzerland": "Switzerland",
|
||||
"syria": "Syria",
|
||||
"taiwan": "Taiwan",
|
||||
"tajikistan": "Tajikistan",
|
||||
"tanzania": "Tanzania",
|
||||
"thailand": "Thailand",
|
||||
"timor_leste": "Timor leste",
|
||||
"togo": "Togo",
|
||||
"tokelau": "Tokelau",
|
||||
"tonga": "Tonga",
|
||||
"trinidad_and_tobago": "Trinidad and tobago",
|
||||
"tunisia": "Tunisia",
|
||||
"turkey": "Turkey",
|
||||
"turkmenistan": "Turkmenistan",
|
||||
"turks_and_caicos_islands": "Turks and caicos islands",
|
||||
"tuvalu": "Tuvalu",
|
||||
"us_virgin_islands": "Us virgin islands",
|
||||
"uganda": "Uganda",
|
||||
"ukraine": "Ukraine",
|
||||
"united_arab_emirates": "United arab emirates",
|
||||
"united_kingdom": "United kingdom",
|
||||
"united_states": "United states",
|
||||
"uruguay": "Uruguay",
|
||||
"uzbekistan": "Uzbekistan",
|
||||
"vanuatu": "Vanuatu",
|
||||
"vatican_city": "Vatican city",
|
||||
"venezuela": "Venezuela",
|
||||
"vietnam": "Vietnam",
|
||||
"wallis_and_futuna": "Wallis and futuna",
|
||||
"western_sahara": "Western sahara",
|
||||
"yemen": "Yemen",
|
||||
"zambia": "Zambia",
|
||||
"zimbabwe": "Zimbabwe"
|
||||
}
|
||||
}
|
||||
|
||||
55
public/locales/fa/authentication.json
Normal file
55
public/locales/fa/authentication.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"loginForm": {
|
||||
"title": "ورود/ثبتنام",
|
||||
"description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.",
|
||||
"emailOrPhoneLabel": "شماره موبایل/ایمیل",
|
||||
"submitButton": "ورود/ثبتنام",
|
||||
"loginWithGoogle": "ورود با گوگل",
|
||||
"emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
|
||||
"phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
|
||||
"thisFieldIsRequired": "این فیلد الزامی است"
|
||||
},
|
||||
"verify": {
|
||||
"verify": "اعتبارسنجی",
|
||||
"a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.",
|
||||
"confirmAndLogin": "تایید و ورود",
|
||||
"confirmAndContinue": "تایید و ادامه",
|
||||
"thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید.",
|
||||
"a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "کد تایید ۴ رقمی به شماره ایمیل شما ارسال شد. لطفا آن را وارد کنید.",
|
||||
"thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
|
||||
"theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
|
||||
"youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
|
||||
"youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد",
|
||||
"resendCodeIn": "ارسال مجدد کد تا",
|
||||
"moreMinute": "دقیقه دیگر",
|
||||
"resendCode": "ارسال مجدد"
|
||||
},
|
||||
"completeSignUp": {
|
||||
"completeSignUp": "تکمیل ثبت نام",
|
||||
"emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "ایمیل {{ email }} با موفقیت تایید شد. برای ادامه لطفا شماره تماس خود را وارد کنید",
|
||||
"phoneNumber": "شماره تماس"
|
||||
},
|
||||
"enterPassword": {
|
||||
"loginWithPassword": "ورود با رمز",
|
||||
"enterThePasswordYouSetForYourAccount": "رمز ورودی که برای اکانت خود تعیین کردید را وارد کنید",
|
||||
"loginPassword": "رمز ورود",
|
||||
"loginWithOneTimeCode": "ورود با کد یکبار مصرف",
|
||||
"iForgotMyPassword": "رمز ورودم را فراموش کردم"
|
||||
},
|
||||
"forgetPassword": {
|
||||
"forgetPassword": "فراموشی رمز",
|
||||
"pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "لطفا برای بازیابی رمز عبور شماره موبایل/ایمیل خود را وارد کنید.",
|
||||
"anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "یک ایمیل حاوی کد بازیابی به این ایمیل ارسال شد",
|
||||
"anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "یک کد بازیابی به این شماره ارسال شد",
|
||||
"confirm": "تایید",
|
||||
"changePassword": "تغییر رمز عبور",
|
||||
"createANewPassword": "یک رمز عبور جدید ایجاد کنید",
|
||||
"newPassword": "رمز عبور جدید",
|
||||
"includingANumber": "شامل عدد",
|
||||
"atLeast8Characters": "حداقل ۸ حرف",
|
||||
"containsAnUppercaseAndLowercaseLetter": "شامل یک حرف بزرگ و کوچک",
|
||||
"ContainsASymbol": "شامل علامت (!@#$%&*^)",
|
||||
"confirmPassword": "تکرار رمز عبور",
|
||||
"passwordChangedSuccessfully": "رمز عبور با موفقیت تغییر یافت"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,192 @@
|
||||
{
|
||||
"labels": {
|
||||
"search": "جست و جو"
|
||||
},
|
||||
"country": {
|
||||
"afghanistan": "افغانستان",
|
||||
"aland_islands": "جزایر آلند",
|
||||
"albania": "آلبانی",
|
||||
"algeria": "الجزایر",
|
||||
"american_samoa": "ساموای آمریکایی",
|
||||
"andorra": "آندورا",
|
||||
"angola": "آنگولا",
|
||||
"anguilla": "آنگویلا",
|
||||
"antarctica": "جنوبگان",
|
||||
"antigua_and_barbuda": "آنتیگوا و باربودا",
|
||||
"argentina": "آرژانتین",
|
||||
"armenia": "ارمنستان",
|
||||
"aruba": "آروبا",
|
||||
"australia": "استرالیا",
|
||||
"austria": "اتریش",
|
||||
"azerbaijan": "آذربایجان",
|
||||
"bahamas": "باهاما",
|
||||
"bahrain": "بحرین",
|
||||
"bangladesh": "بنگلادش",
|
||||
"barbados": "باربادوس",
|
||||
"belarus": "بلاروس",
|
||||
"belgium": "بلژیک",
|
||||
"belize": "بلیز",
|
||||
"benin": "بنین",
|
||||
"bermuda": "برمودا",
|
||||
"bhutan": "بوتان",
|
||||
"bolivia": "بولیوی",
|
||||
"bosnia_and_herzegovina": "بوسنی و هرزگوین",
|
||||
"botswana": "بوتسوانا",
|
||||
"brazil": "برزیل",
|
||||
"british_virgin_islands": "جزایر ویرجین بریتانیا",
|
||||
"brunei": "برونئی",
|
||||
"bulgaria": "بلغارستان",
|
||||
"burkina_faso": "بورکینافاسو",
|
||||
"burundi": "بوروندی",
|
||||
"cambodia": "کامبوج",
|
||||
"cameroon": "کامرون",
|
||||
"canada": "کانادا",
|
||||
"cape_verde": "کیپ ورد",
|
||||
"cayman_islands": "جزایر کیمن",
|
||||
"central_african_republic": "جمهوری آفریقای مرکزی",
|
||||
"chad": "چاد",
|
||||
"chile": "شیلی",
|
||||
"china": "چین",
|
||||
"colombia": "کلمبیا",
|
||||
"comoros": "کومور",
|
||||
"costa_rica": "کاستاریکا",
|
||||
"cote_divoire": "ساحل عاج",
|
||||
"croatia": "کرواسی",
|
||||
"cuba": "کوبا",
|
||||
"cyprus": "قبرس",
|
||||
"czech_republic": "جمهوری چک",
|
||||
"denmark": "دانمارک",
|
||||
"djibouti": "جیبوتی",
|
||||
"dominica": "دومینیکا",
|
||||
"dominican_republic": "جمهوری دومینیکن",
|
||||
"ecuador": "اکوادور",
|
||||
"egypt": "مصر",
|
||||
"el_salvador": "السالوادور",
|
||||
"equatorial_guinea": "گینه استوایی",
|
||||
"eritrea": "اریتره",
|
||||
"estonia": "استونی",
|
||||
"eswatini": "سوازیلند",
|
||||
"ethiopia": "اتیوپی",
|
||||
"fiji": "فیجی",
|
||||
"finland": "فنلاند",
|
||||
"france": "فرانسه",
|
||||
"gabon": "گابن",
|
||||
"gambia": "گامبیا",
|
||||
"georgia": "گرجستان",
|
||||
"germany": "آلمان",
|
||||
"ghana": "غنا",
|
||||
"greece": "یونان",
|
||||
"guatemala": "گواتمالا",
|
||||
"guinea": "گینه",
|
||||
"guinea_bissau": "گینه بیسائو",
|
||||
"guyana": "گویان",
|
||||
"haiti": "هائیتی",
|
||||
"honduras": "هندوراس",
|
||||
"hungary": "مجارستان",
|
||||
"iceland": "ایسلند",
|
||||
"india": "هندوستان",
|
||||
"indonesia": "اندونزی",
|
||||
"iran": "ایران",
|
||||
"iraq": "عراق",
|
||||
"ireland": "ایرلند",
|
||||
"israel": "اسرائیل",
|
||||
"italy": "ایتالیا",
|
||||
"jamaica": "جامائیکا",
|
||||
"japan": "ژاپن",
|
||||
"jordan": "اردن",
|
||||
"kazakhstan": "قزاقستان",
|
||||
"kenya": "کنیا",
|
||||
"kuwait": "کویت",
|
||||
"kyrgyzstan": "قرقیزستان",
|
||||
"laos": "لائوس",
|
||||
"latvia": "لتونی",
|
||||
"lebanon": "لبنان",
|
||||
"lesotho": "لسوتو",
|
||||
"liberia": "لیبریا",
|
||||
"libya": "لیبی",
|
||||
"luxembourg": "لوکزامبورگ",
|
||||
"malaysia": "مالزی",
|
||||
"maldives": "مالدیو",
|
||||
"mali": "مالی",
|
||||
"malta": "مالت",
|
||||
"mauritania": "موریتانی",
|
||||
"mauritius": "موریس",
|
||||
"mexico": "مکزیک",
|
||||
"moldova": "مولداوی",
|
||||
"monaco": "موناکو",
|
||||
"mongolia": "مغولستان",
|
||||
"morocco": "مراکش",
|
||||
"mozambique": "موزامبیک",
|
||||
"myanmar": "میانمار",
|
||||
"namibia": "نامیبیا",
|
||||
"nepal": "نپال",
|
||||
"netherlands": "هلند",
|
||||
"new_zealand": "نیوزیلند",
|
||||
"nicaragua": "نیکاراگوئه",
|
||||
"niger": "نیجر",
|
||||
"nigeria": "نیجریه",
|
||||
"north_korea": "کره شمالی",
|
||||
"north_macedonia": "مقدونیه",
|
||||
"norway": "نروژ",
|
||||
"oman": "عمان",
|
||||
"pakistan": "پاکستان",
|
||||
"palau": "پالائو",
|
||||
"panama": "پاناما",
|
||||
"papua_new_guinea": "پاپوآ گینه نو",
|
||||
"paraguay": "پاراگوئه",
|
||||
"peru": "پرو",
|
||||
"philippines": "فیلیپین",
|
||||
"poland": "لهستان",
|
||||
"portugal": "پرتغال",
|
||||
"qatar": "قطر",
|
||||
"romania": "رومانی",
|
||||
"russia": "روسیه",
|
||||
"rwanda": "رواندا",
|
||||
"saudi_arabia": "عربستان سعودی",
|
||||
"senegal": "سنگال",
|
||||
"serbia": "صربستان",
|
||||
"seychelles": "سیشل",
|
||||
"sierra_leone": "سیرالئون",
|
||||
"singapore": "سنگاپور",
|
||||
"south_africa": "آفریقای جنوبی",
|
||||
"south_korea": "کره جنوبی",
|
||||
"south_sudan": "سودان جنوبی",
|
||||
"spain": "اسپانیا",
|
||||
"sri_lanka": "سریلانکا",
|
||||
"sudan": "سودان",
|
||||
"suriname": "سورینام",
|
||||
"sweden": "سوئد",
|
||||
"switzerland": "سوئیس",
|
||||
"syria": "سوریه",
|
||||
"taiwan": "تایوان",
|
||||
"tajikistan": "تاجیکستان",
|
||||
"tanzania": "تانزانیا",
|
||||
"thailand": "تایلند",
|
||||
"timor_leste": "تیمور شرقی",
|
||||
"togo": "توگو",
|
||||
"tonga": "تونگا",
|
||||
"trinidad_and_tobago": "ترینیداد و توباگو",
|
||||
"tunisia": "تونس",
|
||||
"turkey": "ترکیه",
|
||||
"turkmenistan": "ترکمنستان",
|
||||
"tuvalu": "تووالو",
|
||||
"uganda": "اوگاندا",
|
||||
"ukraine": "اوکراین",
|
||||
"united_arab_emirates": "امارات متحده عربی",
|
||||
"united_kingdom": "انگلستان",
|
||||
"united_states": "ایالات متحده آمریکا",
|
||||
"uruguay": "اروگوئه",
|
||||
"uzbekistan": "ازبکستان",
|
||||
"vanuatu": "وانواتو",
|
||||
"venezuela": "ونزوئلا",
|
||||
"vietnam": "ویتنام",
|
||||
"yemen": "یمن",
|
||||
"zambia": "زامبیا",
|
||||
"zimbabwe": "زیمبابوه"
|
||||
},
|
||||
"messages": {
|
||||
"noResualtFound": "نتیجه ای یافت نشد."
|
||||
},
|
||||
"side": {
|
||||
"account": "حساب کاربری",
|
||||
"personalInfo": "اطلاعات شخصی",
|
||||
|
||||
30
src/assets/logo.svg
Normal file
30
src/assets/logo.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg width="120" height="24" viewBox="0 0 120 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3210_10487)">
|
||||
<path d="M36.0547 18.7496V6.1499H38.3582V16.5897H43.9427V18.7496H36.0547Z" fill="#2979FF"/>
|
||||
<path d="M48.9163 19.0016C48.0553 19.0016 47.2816 18.8216 46.5952 18.4616C45.9088 18.1017 45.3678 17.5797 44.9722 16.8957C44.5767 16.2117 44.3789 15.3897 44.3789 14.4297V14.1417C44.3789 13.1817 44.5767 12.3598 44.9722 11.6758C45.3678 10.9918 45.9088 10.4698 46.5952 10.1098C47.2816 9.74983 48.0553 9.56982 48.9163 9.56982C49.7772 9.56982 50.5509 9.74983 51.2373 10.1098C51.9237 10.4698 52.4647 10.9918 52.8603 11.6758C53.2558 12.3598 53.4536 13.1817 53.4536 14.1417V14.4297C53.4536 15.3897 53.2558 16.2117 52.8603 16.8957C52.4647 17.5797 51.9237 18.1017 51.2373 18.4616C50.5509 18.8216 49.7772 19.0016 48.9163 19.0016ZM48.9163 16.9857C49.591 16.9857 50.1495 16.7636 50.5916 16.3197C51.0337 15.8637 51.2548 15.2157 51.2548 14.3757V14.1957C51.2548 13.3557 51.0337 12.7137 50.5916 12.2697C50.1611 11.8138 49.6027 11.5857 48.9163 11.5857C48.2415 11.5857 47.683 11.8138 47.2409 12.2697C46.7988 12.7137 46.5778 13.3557 46.5778 14.1957V14.3757C46.5778 15.2157 46.7988 15.8637 47.2409 16.3197C47.683 16.7636 48.2415 16.9857 48.9163 16.9857Z" fill="black"/>
|
||||
<path d="M54.2793 14.3217V14.0337C54.2793 13.0977 54.4596 12.2997 54.8203 11.6397C55.1809 10.9678 55.6579 10.4578 56.2513 10.1098C56.8563 9.74983 57.5136 9.56982 58.2233 9.56982C59.0144 9.56982 59.6136 9.71378 60.0208 10.0018C60.428 10.2898 60.7246 10.5898 60.9108 10.9017H61.225V9.82177H63.3889V20.3336C63.3889 20.9456 63.2144 21.4315 62.8654 21.7916C62.5163 22.1636 62.051 22.3495 61.4692 22.3495H55.6754V20.3696H60.7014C61.0272 20.3696 61.19 20.1896 61.19 19.8296V17.5077H60.8759C60.7596 17.6997 60.5967 17.8976 60.3873 18.1017C60.1779 18.2936 59.8987 18.4557 59.5496 18.5876C59.2006 18.7196 58.7585 18.7857 58.2233 18.7857C57.5136 18.7857 56.8563 18.6116 56.2513 18.2637C55.6579 17.9036 55.1809 17.3936 54.8203 16.7336C54.4596 16.0617 54.2793 15.2577 54.2793 14.3217ZM58.8516 16.8056C59.5264 16.8056 60.0906 16.5836 60.5443 16.1397C60.9981 15.6957 61.225 15.0717 61.225 14.2677V14.0877C61.225 13.2717 60.9981 12.6477 60.5443 12.2157C60.1022 11.7718 59.538 11.5498 58.8516 11.5498C58.1767 11.5498 57.6125 11.7718 57.1588 12.2157C56.705 12.6477 56.4781 13.2717 56.4781 14.0877V14.2677C56.4781 15.0717 56.705 15.6957 57.1588 16.1397C57.6125 16.5836 58.1767 16.8056 58.8516 16.8056Z" fill="black"/>
|
||||
<path d="M69.1605 19.0016C68.2995 19.0016 67.5258 18.8216 66.8394 18.4616C66.153 18.1017 65.612 17.5797 65.2164 16.8957C64.8209 16.2117 64.623 15.3897 64.623 14.4297V14.1417C64.623 13.1817 64.8209 12.3598 65.2164 11.6758C65.612 10.9918 66.153 10.4698 66.8394 10.1098C67.5258 9.74983 68.2995 9.56982 69.1605 9.56982C70.0214 9.56982 70.795 9.74983 71.4815 10.1098C72.1679 10.4698 72.7089 10.9918 73.1045 11.6758C73.5 12.3598 73.6978 13.1817 73.6978 14.1417V14.4297C73.6978 15.3897 73.5 16.2117 73.1045 16.8957C72.7089 17.5797 72.1679 18.1017 71.4815 18.4616C70.795 18.8216 70.0214 19.0016 69.1605 19.0016ZM69.1605 16.9857C69.8352 16.9857 70.3937 16.7636 70.8357 16.3197C71.2779 15.8637 71.4989 15.2157 71.4989 14.3757V14.1957C71.4989 13.3557 71.2779 12.7137 70.8357 12.2697C70.4053 11.8138 69.8469 11.5857 69.1605 11.5857C68.4856 11.5857 67.9272 11.8138 67.4851 12.2697C67.043 12.7137 66.822 13.3557 66.822 14.1957V14.3757C66.822 15.2157 67.043 15.8637 67.4851 16.3197C67.9272 16.7636 68.4856 16.9857 69.1605 16.9857Z" fill="black"/>
|
||||
<path d="M74.9411 18.7497V9.82185H77.1401V18.7497H74.9411ZM76.0406 8.77793C75.6446 8.77793 75.3079 8.64588 75.0281 8.38186C74.7608 8.11792 74.627 7.7699 74.627 7.33792C74.627 6.90593 74.7608 6.55794 75.0281 6.29394C75.3079 6.02995 75.6446 5.89795 76.0406 5.89795C76.4479 5.89795 76.7854 6.02995 77.0524 6.29394C77.3201 6.55794 77.4544 6.90593 77.4544 7.33792C77.4544 7.7699 77.3201 8.11792 77.0524 8.38186C76.7854 8.64588 76.4479 8.77793 76.0406 8.77793Z" fill="black"/>
|
||||
<path d="M78.793 22.3495V9.82177H80.9567V10.9017H81.271C81.4682 10.5538 81.7765 10.2478 82.1957 9.98376C82.6142 9.70783 83.2135 9.56982 83.9935 9.56982C84.691 9.56982 85.3367 9.74983 85.93 10.1098C86.524 10.4578 87.001 10.9738 87.361 11.6577C87.7217 12.3417 87.9025 13.1697 87.9025 14.1417V14.4297C87.9025 15.4017 87.7217 16.2297 87.361 16.9136C87.001 17.5976 86.524 18.1196 85.93 18.4796C85.3367 18.8276 84.691 19.0016 83.9935 19.0016C83.47 19.0016 83.0275 18.9356 82.6667 18.8036C82.318 18.6836 82.033 18.5276 81.8117 18.3356C81.6025 18.1316 81.4337 17.9276 81.3055 17.7236H80.9912V22.3495H78.793ZM83.3297 17.0216C84.0167 17.0216 84.5807 16.7997 85.0225 16.3557C85.4762 15.8996 85.7035 15.2397 85.7035 14.3757V14.1957C85.7035 13.3317 85.4762 12.6778 85.0225 12.2337C84.5687 11.7777 84.0047 11.5498 83.3297 11.5498C82.6555 11.5498 82.0907 11.7777 81.637 12.2337C81.1832 12.6778 80.9567 13.3317 80.9567 14.1957V14.3757C80.9567 15.2397 81.1832 15.8996 81.637 16.3557C82.0907 16.7997 82.6555 17.0216 83.3297 17.0216Z" fill="black"/>
|
||||
<path d="M92.7302 19.0016C91.6015 19.0016 90.6767 18.7496 89.9552 18.2456C89.2345 17.7417 88.798 17.0216 88.6465 16.0857L90.6707 15.5457C90.7525 15.9657 90.886 16.2956 91.0727 16.5357C91.27 16.7756 91.5085 16.9497 91.7882 17.0577C92.0785 17.1537 92.3927 17.2016 92.7302 17.2016C93.2425 17.2016 93.6205 17.1117 93.8642 16.9317C94.1087 16.7397 94.231 16.5057 94.231 16.2297C94.231 15.9536 94.1147 15.7437 93.8822 15.5997C93.649 15.4437 93.277 15.3177 92.7655 15.2217L92.2765 15.1317C91.6712 15.0117 91.1192 14.8497 90.619 14.6457C90.118 14.4297 89.7167 14.1357 89.4145 13.7637C89.1122 13.3918 88.9607 12.9118 88.9607 12.3238C88.9607 11.4357 89.275 10.7578 89.9035 10.2898C90.5312 9.80978 91.3577 9.56982 92.3815 9.56982C93.3467 9.56982 94.15 9.79176 94.7897 10.2358C95.4295 10.6798 95.848 11.2618 96.046 11.9818L94.0045 12.6297C93.9115 12.1738 93.7195 11.8497 93.4285 11.6577C93.1487 11.4657 92.8 11.3697 92.3815 11.3697C91.9622 11.3697 91.6427 11.4478 91.4215 11.6038C91.2002 11.7478 91.09 11.9518 91.09 12.2157C91.09 12.5037 91.2062 12.7198 91.4387 12.8637C91.6712 12.9957 91.9855 13.0977 92.3815 13.1697L92.8697 13.2597C93.5215 13.3797 94.1087 13.5418 94.6322 13.7457C95.1677 13.9377 95.5862 14.2197 95.8892 14.5917C96.2027 14.9517 96.3602 15.4437 96.3602 16.0677C96.3602 17.0037 96.0287 17.7297 95.3657 18.2456C94.714 18.7496 93.8357 19.0016 92.7302 19.0016Z" fill="black"/>
|
||||
<path d="M100.8 18.8936C100.125 18.8936 99.5316 18.7376 99.0194 18.4256C98.5191 18.1017 98.1299 17.6577 97.8501 17.0936C97.5711 16.5296 97.4316 15.8817 97.4316 15.1497V9.82178H99.6306V14.9697C99.6306 15.6417 99.7874 16.1456 100.102 16.4817C100.427 16.8176 100.887 16.9857 101.48 16.9857C102.155 16.9857 102.679 16.7577 103.051 16.3017C103.423 15.8337 103.609 15.1857 103.609 14.3577V9.82178H105.808V18.7496H103.644V17.5797H103.33C103.191 17.8797 102.928 18.1736 102.545 18.4616C102.161 18.7496 101.579 18.8936 100.8 18.8936Z" fill="black"/>
|
||||
<path d="M107.469 18.7495V9.82169H109.632V10.7937H109.947C110.098 10.4937 110.349 10.2357 110.698 10.0197C111.046 9.79168 111.506 9.67773 112.076 9.67773C112.693 9.67773 113.188 9.80375 113.56 10.0557C113.932 10.2957 114.217 10.6137 114.415 11.0097H114.729C114.927 10.6257 115.206 10.3077 115.566 10.0557C115.927 9.80375 116.439 9.67773 117.102 9.67773C117.637 9.67773 118.12 9.79771 118.551 10.0377C118.993 10.2657 119.342 10.6197 119.598 11.0997C119.865 11.5677 119.999 12.1616 119.999 12.8817V18.7495H117.8V13.0437C117.8 12.5517 117.678 12.1857 117.434 11.9457C117.19 11.6937 116.846 11.5677 116.404 11.5677C115.904 11.5677 115.514 11.7356 115.235 12.0717C114.967 12.3956 114.834 12.8636 114.834 13.4756V18.7495H112.635V13.0437C112.635 12.5517 112.513 12.1857 112.268 11.9457C112.024 11.6937 111.681 11.5677 111.238 11.5677C110.738 11.5677 110.348 11.7356 110.069 12.0717C109.801 12.3956 109.668 12.8636 109.668 13.4756V18.7495H107.469Z" fill="black"/>
|
||||
<path d="M29.0854 12C29.0854 8.81744 27.8596 5.76516 25.6777 3.51472C23.4958 1.26429 20.5366 4.17348e-07 17.4509 0C14.3653 -4.17347e-07 11.406 1.26429 9.22409 3.51472C7.04218 5.76516 5.81641 8.81744 5.81641 12H9.45224C10.657 12 11.5966 10.9587 12.0379 9.80234C12.3277 9.04317 12.7673 8.3455 13.3375 7.75736C14.4284 6.63215 15.9081 6.00001 17.4509 6.00001C18.9937 6.00001 20.4734 6.63215 21.5644 7.75736C22.6553 8.88258 23.2681 10.4087 23.2681 12H29.0854Z" fill="url(#paint0_linear_3210_10487)"/>
|
||||
<path d="M22.3833 16.5922C22.968 15.1363 23.269 13.5758 23.269 12H29.0856C29.0856 18.6274 23.8766 24 17.4511 24C16.4448 24 15.4683 23.8682 14.5371 23.6205C15.064 23.4805 15.5819 23.3022 16.0868 23.0865C17.4983 22.4835 18.7809 21.5996 19.8613 20.4853C20.9417 19.371 21.7987 18.0481 22.3833 16.5922Z" fill="#FBCF8E"/>
|
||||
<path d="M0 12C-2.18575e-07 13.5759 0.300935 15.1363 0.885623 16.5923C1.47031 18.0482 2.3273 19.371 3.40767 20.4854C4.48803 21.5996 5.77061 22.4835 7.18217 23.0866C8.59373 23.6897 10.1066 24 11.6345 24C13.1623 24 14.6753 23.6897 16.0868 23.0866C17.4984 22.4835 18.781 21.5996 19.8614 20.4854C20.9417 19.371 21.7987 18.0482 22.3834 16.5923C22.9681 15.1363 23.2691 13.5759 23.2691 12H19.6332C18.4284 12 17.4888 13.0414 17.0475 14.1976C17.035 14.2306 17.0221 14.2634 17.0089 14.2962C16.7166 15.0241 16.2881 15.6856 15.7479 16.2427C15.2078 16.7998 14.5664 17.2418 13.8607 17.5433C13.1549 17.8449 12.3985 18.0001 11.6345 18.0001C10.8706 18.0001 10.1141 17.8449 9.40837 17.5433C8.70255 17.2418 8.0613 16.7998 7.52108 16.2427C6.9809 15.6856 6.55241 15.0241 6.26007 14.2962C5.96772 13.5682 5.81726 12.788 5.81726 12H0Z" fill="url(#paint1_linear_3210_10487)"/>
|
||||
<path d="M0.00195312 12C0.00195312 5.37257 5.21089 0 11.6364 0C12.6425 0 13.6187 0.131696 14.5498 0.379295C12.5519 0.91015 10.7088 1.98446 9.22509 3.51473C7.0432 5.76515 5.81744 8.81744 5.81744 12H0.00195312Z" fill="#73E5E2"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3210_10487" x1="29.0854" y1="6.00001" x2="5.81641" y2="6.00001" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#73E5E2"/>
|
||||
<stop offset="1" stop-color="#394F87"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_3210_10487" x1="7.6284e-07" y1="18.0001" x2="23.2691" y2="18.0001" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FBDC8E"/>
|
||||
<stop offset="1" stop-color="#FB958E"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3210_10487">
|
||||
<rect width="120" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
7
src/components/Logo.tsx
Normal file
7
src/components/Logo.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import LogoSvg from '@/assets/logo.svg';
|
||||
|
||||
function Logo() {
|
||||
return <img src={LogoSvg} style={{ width: '150px', height: 'auto' }} />;
|
||||
}
|
||||
|
||||
export default Logo;
|
||||
27
src/components/Toast.tsx
Normal file
27
src/components/Toast.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Alert, Snackbar, type AlertColor } from '@mui/material';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
export interface ToastProps extends PropsWithChildren {
|
||||
color: AlertColor | undefined;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const Toast = ({ color, open, onClose, children }: ToastProps) => {
|
||||
return (
|
||||
<Snackbar
|
||||
sx={{ width: (t) => `calc(100% - ${t.spacing(6)})`, maxWidth: '396px' }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Alert
|
||||
onClose={onClose}
|
||||
severity={color}
|
||||
variant="filled"
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{children}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
||||
124
src/components/components/DigitsInput.tsx
Normal file
124
src/components/components/DigitsInput.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, {
|
||||
useRef,
|
||||
useEffect,
|
||||
type SetStateAction,
|
||||
type Dispatch,
|
||||
useState,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
import { TextField, Stack } from '@mui/material';
|
||||
|
||||
interface DigitInputProps {
|
||||
error: boolean;
|
||||
success: boolean;
|
||||
onChange: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const DigitInput: React.FC<DigitInputProps> = ({
|
||||
onChange,
|
||||
error,
|
||||
success,
|
||||
}) => {
|
||||
const [code, setCode] = useState<string[]>(['', '', '', '']);
|
||||
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRefs.current[0]?.focus();
|
||||
}, []);
|
||||
|
||||
const handleDigitInputValueChange = (value: string[]) => {
|
||||
const formatted = value.filter((char) => char !== '').join('');
|
||||
onChange(formatted);
|
||||
};
|
||||
|
||||
const handleChange = (value: string, index: number) => {
|
||||
if (!/^\d$/.test(value) && value !== '') return;
|
||||
|
||||
const newCode = [...code];
|
||||
newCode[index] = value;
|
||||
setCode(newCode);
|
||||
handleDigitInputValueChange(newCode);
|
||||
|
||||
if (value && index < 4 - 1) {
|
||||
inputRefs.current[index + 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackspace = (
|
||||
event: KeyboardEvent<HTMLDivElement>,
|
||||
index: number,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
if (index >= 0) {
|
||||
handleChange('', index);
|
||||
inputRefs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = (event: React.ClipboardEvent) => {
|
||||
event.preventDefault();
|
||||
const pastedData = event.clipboardData.getData('text').replace(/\D/g, ''); // Remove non-digit characters
|
||||
const newCode = [...code];
|
||||
|
||||
pastedData.split('').forEach((digit, i) => {
|
||||
if (i < code.length) {
|
||||
newCode[i] = digit;
|
||||
}
|
||||
});
|
||||
|
||||
setCode(newCode);
|
||||
handleDigitInputValueChange(newCode);
|
||||
|
||||
// Focus the next empty input after the last pasted character
|
||||
const lastIndex = Math.min(pastedData.length, code.length) - 1;
|
||||
if (lastIndex >= 0 && inputRefs.current[lastIndex]) {
|
||||
inputRefs.current[lastIndex]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row-reverse"
|
||||
alignItems="center"
|
||||
sx={{ gap: 2, width: '100%', my: 4 }}
|
||||
justifyContent="center"
|
||||
>
|
||||
{code.map((digit, index) => (
|
||||
<TextField
|
||||
error={error}
|
||||
color={success ? 'success' : 'primary'}
|
||||
key={index}
|
||||
inputRef={(el) => (inputRefs.current[index] = el)}
|
||||
value={digit}
|
||||
onChange={(e) => handleChange(e.target.value, index)}
|
||||
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
|
||||
onPaste={(e) => handlePaste(e)}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
maxLength: 1,
|
||||
sx: {
|
||||
height: '72px',
|
||||
color: error
|
||||
? 'error.main'
|
||||
: success
|
||||
? 'success.main'
|
||||
: 'text.primary',
|
||||
},
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
fontSize: '48px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
variant="standard"
|
||||
size="medium"
|
||||
sx={{
|
||||
width: '83px',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DigitInput;
|
||||
8
src/components/components/common/Container.tsx
Normal file
8
src/components/components/common/Container.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Box, styled } from '@mui/material';
|
||||
|
||||
export const Container = styled(Box)(() => ({
|
||||
width: '100%',
|
||||
maxWidth: '100vw',
|
||||
height: '100vh',
|
||||
margin: '0 auto',
|
||||
}));
|
||||
21
src/components/components/common/FlexBox.tsx
Normal file
21
src/components/components/common/FlexBox.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Box, styled, type BoxProps } from '@mui/material';
|
||||
|
||||
// Define the props our component will accept
|
||||
interface FlexBoxProps extends BoxProps {
|
||||
direction?: 'row' | 'column';
|
||||
justify?: string;
|
||||
align?: string;
|
||||
}
|
||||
|
||||
export const FlexBox = styled(Box, {
|
||||
// Do not forward these custom props to the DOM element
|
||||
shouldForwardProp: (prop) =>
|
||||
prop !== 'direction' && prop !== 'justify' && prop !== 'align',
|
||||
})<FlexBoxProps>(
|
||||
({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: direction,
|
||||
justifyContent: justify,
|
||||
alignItems: align,
|
||||
}),
|
||||
);
|
||||
19
src/components/components/common/Stack.tsx
Normal file
19
src/components/components/common/Stack.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Box, styled, type BoxProps } from '@mui/material';
|
||||
|
||||
interface StackProps extends BoxProps {
|
||||
direction?: 'row' | 'column';
|
||||
spacing?: number; // Spacing factor (multiplied by theme.spacing)
|
||||
align?: string;
|
||||
}
|
||||
|
||||
export const Stack = styled(Box, {
|
||||
shouldForwardProp: (prop) =>
|
||||
prop !== 'direction' && prop !== 'spacing' && prop !== 'align',
|
||||
})<StackProps>(
|
||||
({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: direction,
|
||||
alignItems: align,
|
||||
gap: theme.spacing(spacing),
|
||||
}),
|
||||
);
|
||||
267
src/countries.ts
Normal file
267
src/countries.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
|
||||
export interface Country {
|
||||
code: string;
|
||||
label: string;
|
||||
phone: CountryCode;
|
||||
}
|
||||
|
||||
export const countries: readonly Country[] = [
|
||||
{ code: 'AF', label: 'country.afghanistan', phone: '+93' },
|
||||
{ code: 'AX', label: 'country.aland_islands', phone: '+358' },
|
||||
{ code: 'AL', label: 'country.albania', phone: '+355' },
|
||||
{ code: 'DZ', label: 'country.algeria', phone: '+213' },
|
||||
{ code: 'AS', label: 'country.american_samoa', phone: '+1684' },
|
||||
{ code: 'AD', label: 'country.andorra', phone: '+376' },
|
||||
{ code: 'AO', label: 'country.angola', phone: '+244' },
|
||||
{ code: 'AI', label: 'country.anguilla', phone: '+1264' },
|
||||
{ code: 'AQ', label: 'country.antarctica', phone: '+672' },
|
||||
{ code: 'AG', label: 'country.antigua_and_barbuda', phone: '+1268' },
|
||||
{ code: 'AR', label: 'country.argentina', phone: '+54' },
|
||||
{ code: 'AM', label: 'country.armenia', phone: '+374' },
|
||||
{ code: 'AW', label: 'country.aruba', phone: '+297' },
|
||||
{ code: 'AU', label: 'country.australia', phone: '+61' },
|
||||
{ code: 'AT', label: 'country.austria', phone: '+43' },
|
||||
{ code: 'AZ', label: 'country.azerbaijan', phone: '+994' },
|
||||
{ code: 'BS', label: 'country.bahamas', phone: '+1242' },
|
||||
{ code: 'BH', label: 'country.bahrain', phone: '+973' },
|
||||
{ code: 'BD', label: 'country.bangladesh', phone: '+880' },
|
||||
{ code: 'BB', label: 'country.barbados', phone: '+1246' },
|
||||
{ code: 'BY', label: 'country.belarus', phone: '+375' },
|
||||
{ code: 'BE', label: 'country.belgium', phone: '+32' },
|
||||
{ code: 'BZ', label: 'country.belize', phone: '+501' },
|
||||
{ code: 'BJ', label: 'country.benin', phone: '+229' },
|
||||
{ code: 'BM', label: 'country.bermuda', phone: '+1441' },
|
||||
{ code: 'BT', label: 'country.bhutan', phone: '+975' },
|
||||
{ code: 'BO', label: 'country.bolivia', phone: '+591' },
|
||||
{ code: 'BA', label: 'country.bosnia_and_herzegovina', phone: '+387' },
|
||||
{ code: 'BW', label: 'country.botswana', phone: '+267' },
|
||||
{ code: 'BR', label: 'country.brazil', phone: '+55' },
|
||||
{
|
||||
code: 'IO',
|
||||
label: 'country.british_indian_ocean_territory',
|
||||
phone: '+246',
|
||||
},
|
||||
{ code: 'VG', label: 'country.british_virgin_islands', phone: '+1284' },
|
||||
{ code: 'BN', label: 'country.brunei', phone: '+673' },
|
||||
{ code: 'BG', label: 'country.bulgaria', phone: '+359' },
|
||||
{ code: 'BF', label: 'country.burkina_faso', phone: '+226' },
|
||||
{ code: 'BI', label: 'country.burundi', phone: '+257' },
|
||||
{ code: 'KH', label: 'country.cambodia', phone: '+855' },
|
||||
{ code: 'CM', label: 'country.cameroon', phone: '+237' },
|
||||
{ code: 'CA', label: 'country.canada', phone: '+1' },
|
||||
{ code: 'CV', label: 'country.cape_verde', phone: '+238' },
|
||||
{ code: 'KY', label: 'country.cayman_islands', phone: '+1345' },
|
||||
{ code: 'CF', label: 'country.central_african_republic', phone: '+236' },
|
||||
{ code: 'TD', label: 'country.chad', phone: '+235' },
|
||||
{ code: 'CL', label: 'country.chile', phone: '+56' },
|
||||
{ code: 'CN', label: 'country.china', phone: '+86' },
|
||||
{ code: 'CX', label: 'country.christmas_island', phone: '+61' },
|
||||
{ code: 'CC', label: 'country.cocos_keeling_islands', phone: '+61' },
|
||||
{ code: 'CO', label: 'country.colombia', phone: '+57' },
|
||||
{ code: 'KM', label: 'country.comoros', phone: '+269' },
|
||||
{ code: 'CG', label: 'country.congo_brazzaville', phone: '+242' },
|
||||
{ code: 'CD', label: 'country.congo_kinshasa', phone: '+243' },
|
||||
{ code: 'CK', label: 'country.cook_islands', phone: '+682' },
|
||||
{ code: 'CR', label: 'country.costa_rica', phone: '+506' },
|
||||
{ code: 'CI', label: 'country.cote_divoire', phone: '+225' },
|
||||
{ code: 'HR', label: 'country.croatia', phone: '+385' },
|
||||
{ code: 'CU', label: 'country.cuba', phone: '+53' },
|
||||
{ code: 'CW', label: 'country.curacao', phone: '+599' },
|
||||
{ code: 'CY', label: 'country.cyprus', phone: '+357' },
|
||||
{ code: 'CZ', label: 'country.czech_republic', phone: '+420' },
|
||||
{ code: 'DK', label: 'country.denmark', phone: '+45' },
|
||||
{ code: 'DJ', label: 'country.djibouti', phone: '+253' },
|
||||
{ code: 'DM', label: 'country.dominica', phone: '+1767' },
|
||||
{ code: 'DO', label: 'country.dominican_republic', phone: '+1' },
|
||||
{ code: 'EC', label: 'country.ecuador', phone: '+593' },
|
||||
{ code: 'EG', label: 'country.egypt', phone: '+20' },
|
||||
{ code: 'SV', label: 'country.el_salvador', phone: '+503' },
|
||||
{ code: 'GQ', label: 'country.equatorial_guinea', phone: '+240' },
|
||||
{ code: 'ER', label: 'country.eritrea', phone: '+291' },
|
||||
{ code: 'EE', label: 'country.estonia', phone: '+372' },
|
||||
{ code: 'SZ', label: 'country.eswatini', phone: '+268' },
|
||||
{ code: 'ET', label: 'country.ethiopia', phone: '+251' },
|
||||
{ code: 'FK', label: 'country.falkland_islands', phone: '+500' },
|
||||
{ code: 'FO', label: 'country.faroe_islands', phone: '+298' },
|
||||
{ code: 'FJ', label: 'country.fiji', phone: '+679' },
|
||||
{ code: 'FI', label: 'country.finland', phone: '+358' },
|
||||
{ code: 'FR', label: 'country.france', phone: '+33' },
|
||||
{ code: 'GF', label: 'country.french_guiana', phone: '+594' },
|
||||
{ code: 'PF', label: 'country.french_polynesia', phone: '+689' },
|
||||
{ code: 'GA', label: 'country.gabon', phone: '+241' },
|
||||
{ code: 'GM', label: 'country.gambia', phone: '+220' },
|
||||
{ code: 'GE', label: 'country.georgia', phone: '+995' },
|
||||
{ code: 'DE', label: 'country.germany', phone: '+49' },
|
||||
{ code: 'GH', label: 'country.ghana', phone: '+233' },
|
||||
{ code: 'GI', label: 'country.gibraltar', phone: '+350' },
|
||||
{ code: 'GR', label: 'country.greece', phone: '+30' },
|
||||
{ code: 'GL', label: 'country.greenland', phone: '+299' },
|
||||
{ code: 'GD', label: 'country.grenada', phone: '+1473' },
|
||||
{ code: 'GP', label: 'country.guadeloupe', phone: '+590' },
|
||||
{ code: 'GU', label: 'country.guam', phone: '+1671' },
|
||||
{ code: 'GT', label: 'country.guatemala', phone: '+502' },
|
||||
{ code: 'GG', label: 'country.guernsey', phone: '+44' },
|
||||
{ code: 'GN', label: 'country.guinea', phone: '+224' },
|
||||
{ code: 'GW', label: 'country.guinea_bissau', phone: '+245' },
|
||||
{ code: 'GY', label: 'country.guyana', phone: '+592' },
|
||||
{ code: 'HT', label: 'country.haiti', phone: '+509' },
|
||||
{ code: 'HN', label: 'country.honduras', phone: '+504' },
|
||||
{ code: 'HK', label: 'country.hong_kong', phone: '+852' },
|
||||
{ code: 'HU', label: 'country.hungary', phone: '+36' },
|
||||
{ code: 'IS', label: 'country.iceland', phone: '+354' },
|
||||
{ code: 'IN', label: 'country.india', phone: '+91' },
|
||||
{ code: 'ID', label: 'country.indonesia', phone: '+62' },
|
||||
{ code: 'IR', label: 'country.iran', phone: '+98' },
|
||||
{ code: 'IQ', label: 'country.iraq', phone: '+964' },
|
||||
{ code: 'IE', label: 'country.ireland', phone: '+353' },
|
||||
{ code: 'IM', label: 'country.isle_of_man', phone: '+44' },
|
||||
{ code: 'IL', label: 'country.israel', phone: '+972' },
|
||||
{ code: 'IT', label: 'country.italy', phone: '+39' },
|
||||
{ code: 'JM', label: 'country.jamaica', phone: '+1876' },
|
||||
{ code: 'JP', label: 'country.japan', phone: '+81' },
|
||||
{ code: 'JE', label: 'country.jersey', phone: '+44' },
|
||||
{ code: 'JO', label: 'country.jordan', phone: '+962' },
|
||||
{ code: 'KZ', label: 'country.kazakhstan', phone: '+7' },
|
||||
{ code: 'KE', label: 'country.kenya', phone: '+254' },
|
||||
{ code: 'KI', label: 'country.kiribati', phone: '+686' },
|
||||
{ code: 'XK', label: 'country.kosovo', phone: '+383' },
|
||||
{ code: 'KW', label: 'country.kuwait', phone: '+965' },
|
||||
{ code: 'KG', label: 'country.kyrgyzstan', phone: '+996' },
|
||||
{ code: 'LA', label: 'country.laos', phone: '+856' },
|
||||
{ code: 'LV', label: 'country.latvia', phone: '+371' },
|
||||
{ code: 'LB', label: 'country.lebanon', phone: '+961' },
|
||||
{ code: 'LS', label: 'country.lesotho', phone: '+266' },
|
||||
{ code: 'LR', label: 'country.liberia', phone: '+231' },
|
||||
{ code: 'LY', label: 'country.libya', phone: '+218' },
|
||||
{ code: 'LI', label: 'country.liechtenstein', phone: '+423' },
|
||||
{ code: 'LT', label: 'country.lithuania', phone: '+370' },
|
||||
{ code: 'LU', label: 'country.luxembourg', phone: '+352' },
|
||||
{ code: 'MO', label: 'country.macau', phone: '+853' },
|
||||
{ code: 'MG', label: 'country.madagascar', phone: '+261' },
|
||||
{ code: 'MW', label: 'country.malawi', phone: '+265' },
|
||||
{ code: 'MY', label: 'country.malaysia', phone: '+60' },
|
||||
{ code: 'MV', label: 'country.maldives', phone: '+960' },
|
||||
{ code: 'ML', label: 'country.mali', phone: '+223' },
|
||||
{ code: 'MT', label: 'country.malta', phone: '+356' },
|
||||
{ code: 'MH', label: 'country.marshall_islands', phone: '+692' },
|
||||
{ code: 'MQ', label: 'country.martinique', phone: '+596' },
|
||||
{ code: 'MR', label: 'country.mauritania', phone: '+222' },
|
||||
{ code: 'MU', label: 'country.mauritius', phone: '+230' },
|
||||
{ code: 'YT', label: 'country.mayotte', phone: '+262' },
|
||||
{ code: 'MX', label: 'country.mexico', phone: '+52' },
|
||||
{ code: 'FM', label: 'country.micronesia', phone: '+691' },
|
||||
{ code: 'MD', label: 'country.moldova', phone: '+373' },
|
||||
{ code: 'MC', label: 'country.monaco', phone: '+377' },
|
||||
{ code: 'MN', label: 'country.mongolia', phone: '+976' },
|
||||
{ code: 'ME', label: 'country.montenegro', phone: '+382' },
|
||||
{ code: 'MS', label: 'country.montserrat', phone: '+1664' },
|
||||
{ code: 'MA', label: 'country.morocco', phone: '+212' },
|
||||
{ code: 'MZ', label: 'country.mozambique', phone: '+258' },
|
||||
{ code: 'MM', label: 'country.myanmar', phone: '+95' },
|
||||
{ code: 'NA', label: 'country.namibia', phone: '+264' },
|
||||
{ code: 'NR', label: 'country.nauru', phone: '+674' },
|
||||
{ code: 'NP', label: 'country.nepal', phone: '+977' },
|
||||
{ code: 'NL', label: 'country.netherlands', phone: '+31' },
|
||||
{ code: 'NC', label: 'country.new_caledonia', phone: '+687' },
|
||||
{ code: 'NZ', label: 'country.new_zealand', phone: '+64' },
|
||||
{ code: 'NI', label: 'country.nicaragua', phone: '+505' },
|
||||
{ code: 'NE', label: 'country.niger', phone: '+227' },
|
||||
{ code: 'NG', label: 'country.nigeria', phone: '+234' },
|
||||
{ code: 'NU', label: 'country.niue', phone: '+683' },
|
||||
{ code: 'NF', label: 'country.norfolk_island', phone: '+672' },
|
||||
{ code: 'KP', label: 'country.north_korea', phone: '+850' },
|
||||
{ code: 'MK', label: 'country.north_macedonia', phone: '+389' },
|
||||
{ code: 'MP', label: 'country.northern_mariana_islands', phone: '+1670' },
|
||||
{ code: 'NO', label: 'country.norway', phone: '+47' },
|
||||
{ code: 'OM', label: 'country.oman', phone: '+968' },
|
||||
{ code: 'PK', label: 'country.pakistan', phone: '+92' },
|
||||
{ code: 'PW', label: 'country.palau', phone: '+680' },
|
||||
{ code: 'PS', label: 'country.palestine', phone: '+970' },
|
||||
{ code: 'PA', label: 'country.panama', phone: '+507' },
|
||||
{ code: 'PG', label: 'country.papua_new_guinea', phone: '+675' },
|
||||
{ code: 'PY', label: 'country.paraguay', phone: '+595' },
|
||||
{ code: 'PE', label: 'country.peru', phone: '+51' },
|
||||
{ code: 'PH', label: 'country.philippines', phone: '+63' },
|
||||
{ code: 'PN', label: 'country.pitcairn_islands', phone: '+64' },
|
||||
{ code: 'PL', label: 'country.poland', phone: '+48' },
|
||||
{ code: 'PT', label: 'country.portugal', phone: '+351' },
|
||||
{ code: 'PR', label: 'country.puerto_rico', phone: '+1' },
|
||||
{ code: 'QA', label: 'country.qatar', phone: '+974' },
|
||||
{ code: 'RE', label: 'country.reunion', phone: '+262' },
|
||||
{ code: 'RO', label: 'country.romania', phone: '+40' },
|
||||
{ code: 'RU', label: 'country.russia', phone: '+7' },
|
||||
{ code: 'RW', label: 'country.rwanda', phone: '+250' },
|
||||
{ code: 'BL', label: 'country.saint_barthelemy', phone: '+590' },
|
||||
{ code: 'SH', label: 'country.saint_helena', phone: '+290' },
|
||||
{ code: 'KN', label: 'country.saint_kitts_and_nevis', phone: '+1869' },
|
||||
{ code: 'LC', label: 'country.saint_lucia', phone: '+1758' },
|
||||
{ code: 'MF', label: 'country.saint_martin', phone: '+590' },
|
||||
{ code: 'PM', label: 'country.saint_pierre_and_miquelon', phone: '+508' },
|
||||
{
|
||||
code: 'VC',
|
||||
label: 'country.saint_vincent_and_the_grenadines',
|
||||
phone: '+1784',
|
||||
},
|
||||
{ code: 'WS', label: 'country.samoa', phone: '+685' },
|
||||
{ code: 'SM', label: 'country.san_marino', phone: '+378' },
|
||||
{ code: 'ST', label: 'country.sao_tome_and_principe', phone: '+239' },
|
||||
{ code: 'SA', label: 'country.saudi_arabia', phone: '+966' },
|
||||
{ code: 'SN', label: 'country.senegal', phone: '+221' },
|
||||
{ code: 'RS', label: 'country.serbia', phone: '+381' },
|
||||
{ code: 'SC', label: 'country.seychelles', phone: '+248' },
|
||||
{ code: 'SL', label: 'country.sierra_leone', phone: '+232' },
|
||||
{ code: 'SG', label: 'country.singapore', phone: '+65' },
|
||||
{ code: 'SX', label: 'country.sint_maarten', phone: '+1721' },
|
||||
{ code: 'SK', label: 'country.slovakia', phone: '+421' },
|
||||
{ code: 'SI', label: 'country.slovenia', phone: '+386' },
|
||||
{ code: 'SB', label: 'country.solomon_islands', phone: '+677' },
|
||||
{ code: 'SO', label: 'country.somalia', phone: '+252' },
|
||||
{ code: 'ZA', label: 'country.south_africa', phone: '+27' },
|
||||
{
|
||||
code: 'GS',
|
||||
label: 'country.south_georgia_and_south_sandwich_islands',
|
||||
phone: '+500',
|
||||
},
|
||||
{ code: 'KR', label: 'country.south_korea', phone: '+82' },
|
||||
{ code: 'SS', label: 'country.south_sudan', phone: '+211' },
|
||||
{ code: 'ES', label: 'country.spain', phone: '+34' },
|
||||
{ code: 'LK', label: 'country.sri_lanka', phone: '+94' },
|
||||
{ code: 'SD', label: 'country.sudan', phone: '+249' },
|
||||
{ code: 'SR', label: 'country.suriname', phone: '+597' },
|
||||
{ code: 'SJ', label: 'country.svalbard_and_jan_mayen', phone: '+47' },
|
||||
{ code: 'SE', label: 'country.sweden', phone: '+46' },
|
||||
{ code: 'CH', label: 'country.switzerland', phone: '+41' },
|
||||
{ code: 'SY', label: 'country.syria', phone: '+963' },
|
||||
{ code: 'TW', label: 'country.taiwan', phone: '+886' },
|
||||
{ code: 'TJ', label: 'country.tajikistan', phone: '+992' },
|
||||
{ code: 'TZ', label: 'country.tanzania', phone: '+255' },
|
||||
{ code: 'TH', label: 'country.thailand', phone: '+66' },
|
||||
{ code: 'TL', label: 'country.timor_leste', phone: '+670' },
|
||||
{ code: 'TG', label: 'country.togo', phone: '+228' },
|
||||
{ code: 'TK', label: 'country.tokelau', phone: '+690' },
|
||||
{ code: 'TO', label: 'country.tonga', phone: '+676' },
|
||||
{ code: 'TT', label: 'country.trinidad_and_tobago', phone: '+1868' },
|
||||
{ code: 'TN', label: 'country.tunisia', phone: '+216' },
|
||||
{ code: 'TR', label: 'country.turkey', phone: '+90' },
|
||||
{ code: 'TM', label: 'country.turkmenistan', phone: '+993' },
|
||||
{ code: 'TC', label: 'country.turks_and_caicos_islands', phone: '+1649' },
|
||||
{ code: 'TV', label: 'country.tuvalu', phone: '+688' },
|
||||
{ code: 'VI', label: 'country.us_virgin_islands', phone: '+1340' },
|
||||
{ code: 'UG', label: 'country.uganda', phone: '+256' },
|
||||
{ code: 'UA', label: 'country.ukraine', phone: '+380' },
|
||||
{ code: 'AE', label: 'country.united_arab_emirates', phone: '+971' },
|
||||
{ code: 'GB', label: 'country.united_kingdom', phone: '+44' },
|
||||
{ code: 'US', label: 'country.united_states', phone: '+1' },
|
||||
{ code: 'UY', label: 'country.uruguay', phone: '+598' },
|
||||
{ code: 'UZ', label: 'country.uzbekistan', phone: '+998' },
|
||||
{ code: 'VU', label: 'country.vanuatu', phone: '+678' },
|
||||
{ code: 'VA', label: 'country.vatican_city', phone: '+39' },
|
||||
{ code: 'VE', label: 'country.venezuela', phone: '+58' },
|
||||
{ code: 'VN', label: 'country.vietnam', phone: '+84' },
|
||||
{ code: 'WF', label: 'country.wallis_and_futuna', phone: '+681' },
|
||||
{ code: 'EH', label: 'country.western_sahara', phone: '+212' },
|
||||
{ code: 'YE', label: 'country.yemen', phone: '+967' },
|
||||
{ code: 'ZM', label: 'country.zambia', phone: '+260' },
|
||||
{ code: 'ZW', label: 'country.zimbabwe', phone: '+263' },
|
||||
];
|
||||
104
src/features/authorization/api/authorizationAPI.ts
Normal file
104
src/features/authorization/api/authorizationAPI.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { ApiResponse } from '@/types/apiResponse';
|
||||
import type { FetchPromise } from '@/types/fetchPromise';
|
||||
import type {
|
||||
CompleteUserInformationRequest,
|
||||
ConfirmEmailOtpRequest,
|
||||
ConfirmForgetPassCodeRequest,
|
||||
ConfirmOtpResponse,
|
||||
ConfirmSmsOtpRequest,
|
||||
GetUserStatusByPhoneNumberOrEmailRequest,
|
||||
GetUserStatusByPhoneNumberOrEmailResponse,
|
||||
LoginOrSignUpWithGoogleRequest,
|
||||
LoginOrSignUpWithGoogleResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
PasswordLoginRequest,
|
||||
ResetPasswordRequest,
|
||||
ResetPasswordResponse,
|
||||
SendEmailOtpRequest,
|
||||
SendForgetPassCodeRequest,
|
||||
SendSmsOtpRequest,
|
||||
} from '../types/userTypes';
|
||||
|
||||
const API_URL = 'https://accounts.business-harmony.com/api';
|
||||
|
||||
export const fetchRequest = <T = ApiResponse>(
|
||||
url: string,
|
||||
body: Object | null,
|
||||
): FetchPromise<T> => {
|
||||
return fetch(`${API_URL}/${url}`, {
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// GetUserStatusByPhoneNumberOrEmail
|
||||
|
||||
export const getUserStatusByPhoneNumberOrEmail = async (
|
||||
body: GetUserStatusByPhoneNumberOrEmailRequest,
|
||||
) => {
|
||||
return fetchRequest<GetUserStatusByPhoneNumberOrEmailResponse>(
|
||||
'User/GetUserStatusByPhoneNumberOrEmail',
|
||||
body,
|
||||
);
|
||||
};
|
||||
|
||||
export const loginOrSignUpWithOtp = async (body: LoginRequest) => {
|
||||
return fetchRequest<LoginResponse>('User/LoginOrSignUpWithOtp', body);
|
||||
};
|
||||
|
||||
export const loginWithPassword = async (body: PasswordLoginRequest) => {
|
||||
return fetchRequest<LoginResponse>('User/LoginWithPassword', body);
|
||||
};
|
||||
|
||||
export const sendSmsOtp = async (body: SendSmsOtpRequest) => {
|
||||
return fetchRequest<ApiResponse>('User/SendSmsOtp', body);
|
||||
};
|
||||
|
||||
export const sendEmailOtp = async (body: SendEmailOtpRequest) => {
|
||||
return fetchRequest<ApiResponse>('User/SendEmailOtp', body);
|
||||
};
|
||||
|
||||
export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => {
|
||||
return fetchRequest<ConfirmOtpResponse>('User/ConfirmSmsOtp', body);
|
||||
};
|
||||
|
||||
export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => {
|
||||
return fetchRequest<ConfirmOtpResponse>('User/ConfirmEmailOtp', body);
|
||||
};
|
||||
|
||||
export const resetPassword = async (body: ResetPasswordRequest) => {
|
||||
return fetchRequest<ResetPasswordResponse>('User/ResetPassword', body);
|
||||
};
|
||||
|
||||
export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => {
|
||||
return fetchRequest<ApiResponse>('User/SendForgetPassCode', body);
|
||||
};
|
||||
|
||||
export const confirmForgetPassCode = async (
|
||||
body: ConfirmForgetPassCodeRequest,
|
||||
) => {
|
||||
return fetchRequest<ConfirmOtpResponse>('User/ConfirmForgetPassCode', body);
|
||||
};
|
||||
|
||||
export const loginOrSignUpWithGoogle = async (
|
||||
body: LoginOrSignUpWithGoogleRequest,
|
||||
) => {
|
||||
return fetchRequest<LoginOrSignUpWithGoogleResponse>(
|
||||
'User/LoginOrSignUpWithGoogle',
|
||||
body,
|
||||
);
|
||||
};
|
||||
|
||||
export const completeUserInformation = async (
|
||||
body: CompleteUserInformationRequest,
|
||||
) => {
|
||||
return fetchRequest<ApiResponse>('User/CompleteUserInformation', body);
|
||||
};
|
||||
|
||||
export const logOut = async () => {
|
||||
return fetchRequest<ApiResponse>('User/LogOut', {});
|
||||
};
|
||||
23
src/features/authorization/components/AuthenticationCard.tsx
Normal file
23
src/features/authorization/components/AuthenticationCard.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Paper } from '@mui/material';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
// Beacuse in the otp verify there is a element outside of the authentication card
|
||||
export const AuthenticationCard = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: 4,
|
||||
p: {
|
||||
xs: 4,
|
||||
md: 6,
|
||||
},
|
||||
marginInline: 2,
|
||||
width: (t) => `calc(100% - ${t.spacing(2)})`,
|
||||
maxWidth: '552px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useState, type JSX } from 'react';
|
||||
import { LoginRegisterForm } from './LoginRegiserForm';
|
||||
import type { AuthMode, AuthStep, AuthType } from '../../types/authTypes';
|
||||
import { OtpVerifyForm } from './OtpVerifyForm';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import { CompleteSignUp } from './CompleteSignUp';
|
||||
import { EnterPasswordForm } from './EnterPasswordForm';
|
||||
import { UserStatus } from '../../types/userTypes';
|
||||
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||
import { VerifyPhoneNumber } from './VerifyPhoneNumber';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
export const AuthenticationSteps = (): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
const DEFAULT_RETURN_URL = '/profile';
|
||||
const [searchParams] = useSearchParams();
|
||||
const authReturnUrl: string =
|
||||
searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL;
|
||||
const [authMode, setAuthMode] = useState<AuthMode>('register');
|
||||
const [authType, setAuthType] = useState<AuthType>('phone');
|
||||
const [currentStep, setCurrentStep] = useState<AuthStep>('emailOrPhone');
|
||||
const [loginRegisterValue, setLoginRegisterValue] = useState<string>('');
|
||||
const [countryCode, setCountryCode] = useState<CountryCode>('+98');
|
||||
const [addPhoneCountryCode, setAddPhoneCountryCode] =
|
||||
useState<CountryCode>('+98');
|
||||
const [addedPhoneNumberValue, setAddedPhoneNumberValue] =
|
||||
useState<string>('');
|
||||
|
||||
const handleLoginRegister = (value: string, userStatus: UserStatus) => {
|
||||
setAuthType(isNumeric(value) ? 'phone' : 'email');
|
||||
|
||||
switch (userStatus) {
|
||||
case UserStatus.NotRegistered:
|
||||
setAuthMode('register');
|
||||
setCurrentStep('verify');
|
||||
break;
|
||||
|
||||
case UserStatus.RegisteredWithoutPassword:
|
||||
setAuthMode('login');
|
||||
setCurrentStep('verify');
|
||||
|
||||
break;
|
||||
|
||||
case UserStatus.RegisteredWithPassword:
|
||||
setAuthMode('login');
|
||||
setCurrentStep('enterPassword');
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserLoggedIn = (userId: GUID) => {
|
||||
localStorage.setItem('userID', userId);
|
||||
|
||||
redirectToReturnUrl();
|
||||
};
|
||||
|
||||
const handleConfrimPhoneNumber = (userId: GUID) => {
|
||||
localStorage.setItem('userID', userId);
|
||||
|
||||
setCurrentStep('addPhoneNumber');
|
||||
};
|
||||
|
||||
const handlePhoneNumberVerified = () => {
|
||||
redirectToReturnUrl();
|
||||
};
|
||||
|
||||
const redirectToReturnUrl = () => {
|
||||
if (authReturnUrl === DEFAULT_RETURN_URL) {
|
||||
navigate(DEFAULT_RETURN_URL);
|
||||
} else {
|
||||
location.href = authReturnUrl;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentStep === 'emailOrPhone' && (
|
||||
<LoginRegisterForm
|
||||
authReturnUrl={authReturnUrl}
|
||||
onGoogleAuthenticated={handleUserLoggedIn}
|
||||
countryCode={countryCode}
|
||||
setCountryCode={setCountryCode}
|
||||
loginRegisterValue={loginRegisterValue}
|
||||
setLoginRegisterValue={setLoginRegisterValue}
|
||||
authType={authType}
|
||||
setAuthType={setAuthType}
|
||||
onLoginRegisterSubmit={handleLoginRegister}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 'verify' && (
|
||||
<OtpVerifyForm
|
||||
onVerifyPhoneNumber={handleConfrimPhoneNumber}
|
||||
authReturnUrl={authReturnUrl}
|
||||
countryCode={countryCode}
|
||||
onOTPVerified={handleUserLoggedIn}
|
||||
onEditValue={() => setCurrentStep('emailOrPhone')}
|
||||
authMode={authMode}
|
||||
authType={authType}
|
||||
value={loginRegisterValue}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 'enterPassword' && (
|
||||
<EnterPasswordForm
|
||||
authReturnUrl={authReturnUrl}
|
||||
loginRegisterValue={loginRegisterValue}
|
||||
countryCode={countryCode}
|
||||
authType={authType}
|
||||
onLoggedIn={handleUserLoggedIn}
|
||||
onEditValue={() => setCurrentStep('emailOrPhone')}
|
||||
onLoginWithOTP={() => setCurrentStep('verify')}
|
||||
emailOrPhone={loginRegisterValue}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 'addPhoneNumber' && (
|
||||
<CompleteSignUp
|
||||
countryCode={addPhoneCountryCode}
|
||||
setCountryCode={setAddPhoneCountryCode}
|
||||
value={addedPhoneNumberValue}
|
||||
setValue={setAddedPhoneNumberValue}
|
||||
email={loginRegisterValue}
|
||||
onCompleteSignUp={() => setCurrentStep('addedPhoneNumberVerify')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === 'addedPhoneNumberVerify' && (
|
||||
<VerifyPhoneNumber
|
||||
countryCode={countryCode}
|
||||
onEditValue={() => setCurrentStep('addPhoneNumber')}
|
||||
value={addedPhoneNumberValue}
|
||||
onPhoneNumberVerified={handlePhoneNumberVerified}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Button, TextField, Typography } from '@mui/material';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||
import { sendSmsOtp } from '../../api/authorizationAPI';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
|
||||
export interface CompleteSignUpProps {
|
||||
email: string;
|
||||
value: string;
|
||||
setValue: Dispatch<string>;
|
||||
countryCode: CountryCode;
|
||||
setCountryCode: Dispatch<CountryCode>;
|
||||
onCompleteSignUp: (countryCode: string, value: string) => void;
|
||||
}
|
||||
|
||||
export const CompleteSignUp = ({
|
||||
email,
|
||||
value,
|
||||
setValue,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
onCompleteSignUp,
|
||||
}: CompleteSignUpProps) => {
|
||||
const { t } = useTranslation('authentication');
|
||||
const [error, setError] = useState<string>();
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [touched, setTouched] = useState<boolean>(false);
|
||||
const inputError: boolean = touched && !!error;
|
||||
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false);
|
||||
|
||||
const isPhoneValid = (code: string, phone: string) => {
|
||||
const phoneNumber = parsePhoneNumberFromString(code + phone);
|
||||
|
||||
return phoneNumber && phoneNumber.isValid();
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
|
||||
handleValueError();
|
||||
};
|
||||
|
||||
const handleValueError = () => {
|
||||
if (!value) {
|
||||
setError(t('loginForm.thisFieldIsRequired'));
|
||||
}
|
||||
if (!isPhoneValid(countryCode, value)) {
|
||||
setError(t('loginForm.phoneNumberIsInvalid'));
|
||||
} else {
|
||||
setError(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompleteSignUp = async () => {
|
||||
handleValueError();
|
||||
|
||||
if (!value || !isPhoneValid(countryCode, value)) {
|
||||
inputRef.current?.focus();
|
||||
} else {
|
||||
setSendOtpLoading(true);
|
||||
|
||||
await sendSmsOtp({ phoneNumber: countryCode + value });
|
||||
onCompleteSignUp(countryCode, value);
|
||||
|
||||
setSendOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Typography variant="h5" sx={{ mb: 0.5 }}>
|
||||
{t('completeSignUp.completeSignUp')}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||
{t(
|
||||
'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
|
||||
{ email },
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('completeSignUp.phoneNumber')}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={true}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
|
||||
<Button loading={sendOtpLoading} onClick={handleCompleteSignUp}>
|
||||
{t('verify.confirmAndContinue')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||
import {
|
||||
loginWithPassword,
|
||||
sendEmailOtp,
|
||||
sendSmsOtp,
|
||||
} from '../../api/authorizationAPI';
|
||||
import type { PasswordLoginRequest } from '../../types/userTypes';
|
||||
|
||||
export interface EnterPasswordFormProps {
|
||||
onEditValue: () => void;
|
||||
onLoginWithOTP: () => void;
|
||||
onLoggedIn: (userId: GUID) => void;
|
||||
emailOrPhone: string;
|
||||
authType: AuthType;
|
||||
loginRegisterValue: string;
|
||||
countryCode: CountryCode;
|
||||
authReturnUrl: string;
|
||||
}
|
||||
|
||||
export const EnterPasswordForm = ({
|
||||
onEditValue,
|
||||
onLoginWithOTP,
|
||||
onLoggedIn,
|
||||
emailOrPhone,
|
||||
authType,
|
||||
loginRegisterValue,
|
||||
countryCode,
|
||||
authReturnUrl,
|
||||
}: EnterPasswordFormProps) => {
|
||||
const { t } = useTranslation('authentication');
|
||||
const [passValue, setPassValue] = useState<string>('');
|
||||
const [inputTouched, setInputTouched] = useState<boolean>(false);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [loginLoading, setLoginLoading] = useState<boolean>(false);
|
||||
const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState<boolean>();
|
||||
const [loginAlertOpen, setLoginAlertOpen] = useState<boolean>(false);
|
||||
const [loginFailedMessage, setLoginFailedMessage] = useState<string>('');
|
||||
const [sendOtpLoading, setSendOtpLoading] = useState<boolean>(false);
|
||||
|
||||
const handleBlur = () => {
|
||||
setInputTouched(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!passValue) {
|
||||
inputRef.current?.focus();
|
||||
} else {
|
||||
setLoginLoading(true);
|
||||
|
||||
const apiRequest: PasswordLoginRequest = {
|
||||
phoneNumber:
|
||||
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
|
||||
email: authType === 'email' ? loginRegisterValue : undefined,
|
||||
password: passValue,
|
||||
returnUrl: authReturnUrl,
|
||||
};
|
||||
const result = await loginWithPassword(apiRequest);
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (jsonRes.success) {
|
||||
setIsLoginStatusSuccess(true);
|
||||
onLoggedIn(jsonRes.userId);
|
||||
} else {
|
||||
setIsLoginStatusSuccess(false);
|
||||
setLoginFailedMessage(jsonRes.message);
|
||||
}
|
||||
setLoginAlertOpen(true);
|
||||
setLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginWithOtp = async () => {
|
||||
setSendOtpLoading(true);
|
||||
|
||||
if (authType === 'phone') {
|
||||
await sendSmsOtp({ phoneNumber: countryCode + loginRegisterValue });
|
||||
} else {
|
||||
await sendEmailOtp({ email: loginRegisterValue });
|
||||
}
|
||||
|
||||
setSendOtpLoading(false);
|
||||
onLoginWithOTP();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={loginAlertOpen}
|
||||
onClose={() => setLoginAlertOpen(false)}
|
||||
color={!isLoginStatusSuccess ? 'error' : 'success'}
|
||||
>
|
||||
{!isLoginStatusSuccess
|
||||
? loginFailedMessage
|
||||
: t('verify.youHaveSuccessfullyLoggedIn')}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('enterPassword.loginWithPassword')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditValue}
|
||||
>
|
||||
{emailOrPhone}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||
{t('enterPassword.enterThePasswordYouSetForYourAccount')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label={t('enterPassword.loginPassword')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={passValue}
|
||||
inputRef={inputRef}
|
||||
onChange={(e) => setPassValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
error={!passValue && inputTouched}
|
||||
helperText={
|
||||
!passValue && inputTouched ? t('loginForm.thisFieldIsRequired') : ''
|
||||
}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { sx: { lineHeight: 1.5 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <Eye /> : <EyeSlash />}
|
||||
</IconButton>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleLoginWithOtp}
|
||||
sx={{ width: 'auto', mb: 2 }}
|
||||
variant="text"
|
||||
loading={sendOtpLoading}
|
||||
endIcon={<ArrowLeft />}
|
||||
>
|
||||
{t('enterPassword.loginWithOneTimeCode')}
|
||||
</Button>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<Button loading={loginLoading} onClick={handleSubmit}>
|
||||
{t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
<Link to="/forget-password">
|
||||
<Button variant="text">{t('enterPassword.iForgotMyPassword')}</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Button } from '@mui/material';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { GoogleCodeClientResponse } from '../../types/userTypes';
|
||||
import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
|
||||
import type { GUID } from '@/types/commonTypes';
|
||||
import { Google } from 'iconsax-react';
|
||||
|
||||
export interface GoogleAuthenticationProps {
|
||||
disabled: boolean;
|
||||
authReturnUrl: string;
|
||||
onGoogleAuthenticated: (userId: GUID) => void;
|
||||
}
|
||||
|
||||
export const GoogleAuthentication = ({
|
||||
disabled,
|
||||
authReturnUrl,
|
||||
onGoogleAuthenticated,
|
||||
}: GoogleAuthenticationProps) => {
|
||||
const { t } = useTranslation('authentication');
|
||||
const [loginWithGoogleLoading, setLoginWithGoogleLoading] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const clientRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://accounts.google.com/gsi/client';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
document.body.appendChild(script);
|
||||
|
||||
script.onload = () => {
|
||||
clientRef.current = google.accounts.oauth2.initCodeClient({
|
||||
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
|
||||
scope: 'openid email profile',
|
||||
ux_mode: 'popup',
|
||||
response_type: 'id_token',
|
||||
callback: async (resp: GoogleCodeClientResponse) => {
|
||||
setLoginWithGoogleLoading(true);
|
||||
|
||||
const result = await loginOrSignUpWithGoogle({
|
||||
idToken: resp.id_token,
|
||||
returnUrl: authReturnUrl,
|
||||
});
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (jsonRes.success) {
|
||||
onGoogleAuthenticated(jsonRes.userId);
|
||||
} else {
|
||||
// Todo: Add useToast to handle error
|
||||
}
|
||||
|
||||
setLoginWithGoogleLoading(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleGoogleLogin = () => {
|
||||
if (clientRef.current) {
|
||||
clientRef.current.requestCode();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={disabled}
|
||||
loading={loginWithGoogleLoading}
|
||||
variant="outlined"
|
||||
startIcon={<Google variant="Bold" />}
|
||||
>
|
||||
{t('loginForm.loginWithGoogle')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Button, Stack, TextField, Typography } from '@mui/material';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import { isEmail } from '@/utils/regexes/isEmail';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||
import type { UserStatus } from '../../types/userTypes';
|
||||
import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||
import { GoogleAuthentication } from './GoogleAuthentication';
|
||||
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
|
||||
|
||||
export interface LoginRegisterFormProps {
|
||||
loginRegisterValue: string;
|
||||
setLoginRegisterValue: Dispatch<string>;
|
||||
countryCode: CountryCode;
|
||||
setCountryCode: Dispatch<CountryCode>;
|
||||
authType: AuthType;
|
||||
setAuthType: Dispatch<AuthType>;
|
||||
onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void;
|
||||
authReturnUrl: string;
|
||||
onGoogleAuthenticated: (userId: GUID) => void;
|
||||
}
|
||||
|
||||
export function LoginRegisterForm({
|
||||
loginRegisterValue,
|
||||
setLoginRegisterValue,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
authType,
|
||||
setAuthType,
|
||||
onLoginRegisterSubmit,
|
||||
authReturnUrl,
|
||||
onGoogleAuthenticated,
|
||||
}: LoginRegisterFormProps) {
|
||||
const [checkStatusLoading, setCheckStatusLoading] = useState<boolean>(false);
|
||||
const { t } = useTranslation('authentication');
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [error, setError] = useState<string>();
|
||||
const [touched, setTouched] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const inputError: boolean = touched && !!error;
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
setLoginRegisterValue(newValue);
|
||||
|
||||
// If the new value contains only digits (or is empty), it's a phone number
|
||||
if (isNumeric(newValue)) {
|
||||
setAuthType('phone');
|
||||
} else {
|
||||
setAuthType('email');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
validateInput(loginRegisterValue, authType);
|
||||
};
|
||||
|
||||
const validateInput = (
|
||||
value: string,
|
||||
authType: AuthType,
|
||||
setErrors: boolean = true,
|
||||
): boolean => {
|
||||
if (!value) {
|
||||
if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authType === 'email' && !isEmail(value)) {
|
||||
if (setErrors) setError(t('loginForm.emailIsInvalid'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
|
||||
if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (setErrors) setError(undefined);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (validateInput(loginRegisterValue, authType, false)) {
|
||||
setCheckStatusLoading(true);
|
||||
const result = await getUserStatusByPhoneNumberOrEmail({
|
||||
phoneNumber:
|
||||
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
|
||||
email: authType === 'email' ? loginRegisterValue : undefined,
|
||||
});
|
||||
const jsonResult = await result.json();
|
||||
|
||||
if (jsonResult.success) {
|
||||
onLoginRegisterSubmit(loginRegisterValue, jsonResult.userStatus);
|
||||
} else {
|
||||
setErrorMessage(jsonResult.message);
|
||||
}
|
||||
setCheckStatusLoading(false);
|
||||
} else {
|
||||
inputRef.current?.focus();
|
||||
validateInput(loginRegisterValue, authType);
|
||||
}
|
||||
};
|
||||
|
||||
const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
color="error"
|
||||
onClose={() => setErrorMessage(undefined)}
|
||||
open={!!errorMessage}
|
||||
>
|
||||
{errorMessage}
|
||||
</Toast>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">{t('loginForm.title')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('loginForm.description')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('loginForm.emailOrPhoneLabel')}
|
||||
value={loginRegisterValue}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button loading={checkStatusLoading} onClick={handleSubmit}>
|
||||
{t('loginForm.submitButton')}
|
||||
</Button>
|
||||
|
||||
<GoogleAuthentication
|
||||
authReturnUrl={authReturnUrl}
|
||||
onGoogleAuthenticated={onGoogleAuthenticated}
|
||||
disabled={checkStatusLoading}
|
||||
/>
|
||||
</Stack>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
import { Edit2 } from 'iconsax-react';
|
||||
import DigitInput from '@/components/components/DigitsInput';
|
||||
import type { AuthMode, AuthType } from '../../types/authTypes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import type { LoginRequest } from '../../types/userTypes';
|
||||
import {
|
||||
loginOrSignUpWithOtp,
|
||||
sendEmailOtp,
|
||||
sendSmsOtp,
|
||||
} from '../../api/authorizationAPI';
|
||||
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||
|
||||
interface OtpVerifyFormProps {
|
||||
value: string;
|
||||
countryCode: CountryCode;
|
||||
authType: AuthType;
|
||||
authMode: AuthMode;
|
||||
onEditValue: () => void;
|
||||
onOTPVerified: (userId: GUID) => void;
|
||||
onVerifyPhoneNumber: (userId: GUID) => void;
|
||||
authReturnUrl: string;
|
||||
}
|
||||
|
||||
export function OtpVerifyForm({
|
||||
value,
|
||||
countryCode,
|
||||
authType,
|
||||
authMode,
|
||||
onEditValue,
|
||||
onOTPVerified,
|
||||
onVerifyPhoneNumber,
|
||||
authReturnUrl,
|
||||
}: OtpVerifyFormProps) {
|
||||
const [otpCode, setOtpCode] = useState<string>('');
|
||||
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||
const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||
useState<boolean>(false);
|
||||
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
||||
const { t } = useTranslation('authentication');
|
||||
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setResendTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [resendTimer]);
|
||||
|
||||
const handleResendOTPCode = async () => {
|
||||
setResendLoading(true);
|
||||
|
||||
if (authType === 'phone') {
|
||||
await sendSmsOtp({ phoneNumber: countryCode + value });
|
||||
} else {
|
||||
await sendEmailOtp({ email: value });
|
||||
}
|
||||
|
||||
setResendTimer(120);
|
||||
setCanResend(false);
|
||||
setResendLoading(false);
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = seconds % 60;
|
||||
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const handleVerifyOTP = () => {
|
||||
if (!otpCode || otpCode.length < 4) {
|
||||
setOtpDigitInvalid(true);
|
||||
} else {
|
||||
handleLoginOrSignUp();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginOrSignUp = async () => {
|
||||
setOtpDigitInvalid(false);
|
||||
setVerifyStatusLoading(true);
|
||||
|
||||
const loginRequest: LoginRequest = {
|
||||
otpCode: otpCode,
|
||||
phoneNumber: authType === 'phone' ? countryCode + value : undefined,
|
||||
email: authType === 'email' ? value : undefined,
|
||||
returnUrl: authReturnUrl,
|
||||
};
|
||||
const result = await loginOrSignUpWithOtp(loginRequest);
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (jsonRes.success) {
|
||||
setVerifyStatus('success');
|
||||
|
||||
if (jsonRes.registeredWithOutPhoneNumber) {
|
||||
onVerifyPhoneNumber(jsonRes.userId);
|
||||
} else {
|
||||
onOTPVerified(jsonRes.userId);
|
||||
}
|
||||
} else {
|
||||
setVerifyStatus('failed');
|
||||
setErrorMessage(jsonRes.message);
|
||||
}
|
||||
|
||||
setVerifyAlertOpen(true);
|
||||
setVerifyStatusLoading(false);
|
||||
};
|
||||
|
||||
const otpMessage = (): string => {
|
||||
if (authType === 'phone' && authMode === 'login') {
|
||||
return t(
|
||||
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
|
||||
);
|
||||
} else if (authType === 'phone' && authMode === 'register') {
|
||||
return t(
|
||||
'verify.thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount',
|
||||
);
|
||||
} else if (authType === 'email' && authMode === 'login') {
|
||||
return t(
|
||||
'verify.a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt',
|
||||
);
|
||||
} else if (authType === 'email' && authMode === 'register') {
|
||||
return t(
|
||||
'verify.thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount',
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const toastMessage =
|
||||
verifyStatus === 'failed'
|
||||
? (errorMessage ?? t('verify.theVerificationCodeIsIncorrect'))
|
||||
: verifyStatus === 'success' && authMode === 'register'
|
||||
? t('verify.youHaveSuccessfullySignedIn')
|
||||
: verifyStatus === 'success' && authMode === 'login'
|
||||
? t('verify.youHaveSuccessfullyLoggedIn')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={verifyAlertOpen}
|
||||
onClose={() => setVerifyAlertOpen(false)}
|
||||
color={verifyStatus === 'failed' ? 'error' : 'success'}
|
||||
>
|
||||
{toastMessage}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditValue}
|
||||
>
|
||||
{authType === 'phone' ? countryCode + value : value}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{otpMessage()}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||
success={verifyStatus === 'success'}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||
{authMode === 'register'
|
||||
? t('verify.confirmAndContinue')
|
||||
: t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
loading={resendLoading}
|
||||
sx={{ width: 'auto' }}
|
||||
onClick={canResend ? handleResendOTPCode : undefined}
|
||||
>
|
||||
{canResend && t('verify.resendCode')}
|
||||
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
import { Edit2 } from 'iconsax-react';
|
||||
import DigitInput from '@/components/components/DigitsInput';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import type { ConfirmSmsOtpRequest } from '../../types/userTypes';
|
||||
import { confirmSmsOtp, sendSmsOtp } from '../../api/authorizationAPI';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
|
||||
interface VerifyPhoneNumberProps {
|
||||
value: string;
|
||||
countryCode: CountryCode;
|
||||
onEditValue: () => void;
|
||||
onPhoneNumberVerified: () => void;
|
||||
}
|
||||
|
||||
export function VerifyPhoneNumber({
|
||||
value,
|
||||
countryCode,
|
||||
onEditValue,
|
||||
onPhoneNumberVerified,
|
||||
}: VerifyPhoneNumberProps) {
|
||||
const [otpCode, setOtpCode] = useState<string>('');
|
||||
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||
const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||
useState<boolean>(false);
|
||||
const [verifyAlertOpen, setVerifyAlertOpen] = useState<boolean>(false);
|
||||
const { t } = useTranslation('authentication');
|
||||
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setResendTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [resendTimer]);
|
||||
|
||||
const handleResendOTPCode = async () => {
|
||||
setResendLoading(true);
|
||||
|
||||
await sendSmsOtp({ phoneNumber: countryCode + value });
|
||||
|
||||
setResendTimer(120);
|
||||
setCanResend(false);
|
||||
setResendLoading(false);
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = seconds % 60;
|
||||
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const handleVerifyOTP = async () => {
|
||||
if (!otpCode || otpCode.length < 4) {
|
||||
setOtpDigitInvalid(true);
|
||||
} else {
|
||||
setOtpDigitInvalid(false);
|
||||
setVerifyStatusLoading(true);
|
||||
|
||||
const confirmSmsOtpRequest: ConfirmSmsOtpRequest = {
|
||||
otpCode: otpCode,
|
||||
phoneNumber: countryCode + value,
|
||||
};
|
||||
const result = await confirmSmsOtp(confirmSmsOtpRequest);
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (jsonRes.success) {
|
||||
setVerifyStatus('success');
|
||||
onPhoneNumberVerified();
|
||||
} else {
|
||||
setVerifyStatus('failed');
|
||||
setErrorMessage(jsonRes.message);
|
||||
}
|
||||
|
||||
setVerifyAlertOpen(true);
|
||||
setVerifyStatusLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyAlertMessage = (): string => {
|
||||
if (verifyStatus === 'failed') {
|
||||
return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
|
||||
} else {
|
||||
return t('verify.youHaveSuccessfullyLoggedIn');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={verifyAlertOpen}
|
||||
onClose={() => setVerifyAlertOpen(false)}
|
||||
color={verifyStatus === 'failed' ? 'error' : 'success'}
|
||||
>
|
||||
{verifyAlertMessage()}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">{t('verify.verify')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditValue}
|
||||
>
|
||||
{countryCode + value}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{t(
|
||||
'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||
success={verifyStatus === 'success'}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||
{t('verify.confirmAndLogin')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
loading={resendLoading}
|
||||
sx={{ width: 'auto' }}
|
||||
onClick={canResend ? handleResendOTPCode : undefined}
|
||||
>
|
||||
{canResend && t('verify.resendCode')}
|
||||
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
254
src/features/authorization/components/CountryCodeSelector.tsx
Normal file
254
src/features/authorization/components/CountryCodeSelector.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import {
|
||||
Box,
|
||||
InputAdornment,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useMemo, useRef, useState, type RefObject } from 'react';
|
||||
import { ArrowDown2 } from 'iconsax-react';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { countries, type Country } from '../../../countries';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
interface CountryCodeSelectorProps {
|
||||
show: boolean;
|
||||
value: CountryCode;
|
||||
onChange: (newValue: CountryCode) => void;
|
||||
menuAnchor: HTMLElement | null;
|
||||
onCloseFocusRef: RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An animated country code adornment that fades and slides into view.
|
||||
* Its visibility is controlled by the `show` prop.
|
||||
*/
|
||||
export function CountryCodeSelector({
|
||||
show,
|
||||
value,
|
||||
onChange,
|
||||
menuAnchor,
|
||||
onCloseFocusRef,
|
||||
}: CountryCodeSelectorProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const open = Boolean(anchorEl);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const selectedCountry =
|
||||
countries.find((c) => c.phone === value) || countries[0];
|
||||
|
||||
const handleClick = () => {
|
||||
setAnchorEl(menuAnchor);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setTimeout(() => {
|
||||
setAnchorEl(null);
|
||||
}, 0);
|
||||
setTimeout(() => {
|
||||
onCloseFocusRef.current?.focus();
|
||||
}, 100);
|
||||
setSearchTerm(''); // Reset search on close
|
||||
};
|
||||
|
||||
const handleSelect = (country: Country) => {
|
||||
onChange(country.phone);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMenuEntered = () => {
|
||||
// Focus the input field after the menu has finished opening
|
||||
searchInputRef.current?.focus();
|
||||
};
|
||||
|
||||
const filteredCountries = useMemo(
|
||||
() =>
|
||||
countries.filter(
|
||||
(country) =>
|
||||
t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
country.phone.includes(searchTerm),
|
||||
),
|
||||
[searchTerm],
|
||||
);
|
||||
|
||||
return (
|
||||
<InputAdornment
|
||||
position={i18n.dir() === 'rtl' ? 'start' : 'end'}
|
||||
sx={{
|
||||
mx: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
// Animate width and opacity based on the 'show' prop
|
||||
width: show ? 'auto' : 0,
|
||||
opacity: show ? 1 : 0,
|
||||
transition: (theme) =>
|
||||
theme.transitions.create(['width', 'opacity'], {
|
||||
duration: theme.transitions.duration.standard,
|
||||
}),
|
||||
|
||||
// Prevent content from wrapping or spilling out during animation
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
// layout styles
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.25,
|
||||
pl: show ? 0.25 : 0,
|
||||
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* This inner Box prevents the content from being squeezed during the transition */}
|
||||
<ArrowDown2 size="24" variant="Bold" />
|
||||
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.primary"
|
||||
component="div"
|
||||
sx={{ direction: 'rtl' }} // TODO: need to fixed for both en and fa
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
|
||||
<ReactCountryFlag
|
||||
countryCode={selectedCountry.code}
|
||||
svg
|
||||
style={{
|
||||
height: '1.5rem',
|
||||
width: '1.5rem',
|
||||
// TODO: Check alignment for better styling definition
|
||||
marginTop: '-2px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
slotProps={{
|
||||
list: {
|
||||
sx: {
|
||||
// Set the width to match the anchor element's width
|
||||
width: menuWidth,
|
||||
height: '18.75rem',
|
||||
},
|
||||
},
|
||||
transition: {
|
||||
onEntered: handleMenuEntered,
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
p: 1,
|
||||
backgroundColor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
inputRef={searchInputRef}
|
||||
size="small"
|
||||
fullWidth
|
||||
label={t('labels.search')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Can improve preformance with using virtual scrolling */}
|
||||
<Box sx={{ width: '100%', maxHeight: '14.75rem', overflow: 'auto' }}>
|
||||
{filteredCountries.length === 0 ? (
|
||||
<MenuItem disabled>
|
||||
<ListItem>
|
||||
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
) : (
|
||||
filteredCountries.map((country) => (
|
||||
<MenuItem
|
||||
key={country.code}
|
||||
selected={country.phone === value}
|
||||
onClick={() => handleSelect(country)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ReactCountryFlag
|
||||
countryCode={country.code}
|
||||
svg
|
||||
style={{
|
||||
height: '1.125rem',
|
||||
width: '1.125rem',
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t(country.label)} />
|
||||
<Typography color="text.secondary">
|
||||
{country.phone}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* virtual scrolling */}
|
||||
{/* <Virtuoso
|
||||
style={{ height: '14.75rem' }} // Adjust height to account for the search bar
|
||||
data={filteredCountries}
|
||||
components={{
|
||||
EmptyPlaceholder: () => (
|
||||
<MenuItem disabled>
|
||||
<ListItemText>{t('messages.noResultFound')}</ListItemText>
|
||||
</MenuItem>
|
||||
),
|
||||
}}
|
||||
initialTopMostItemIndex={countries.indexOf(selectedCountry)}
|
||||
itemContent={(_, country) => (
|
||||
<MenuItem
|
||||
key={country.code}
|
||||
selected={country.phone === value}
|
||||
onClick={() => handleSelect(country)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ReactCountryFlag
|
||||
countryCode={country.code}
|
||||
svg
|
||||
style={{ fontSize: '1.25em', lineHeight: '1.25em' }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={country.label} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{country.phone}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
/> */}
|
||||
</Menu>
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { containsNumber } from '@/utils/regexes/containsNumber';
|
||||
import { containsSymbol } from '@/utils/regexes/containsSymbol';
|
||||
import { least8Chars } from '@/utils/regexes/least8Chars';
|
||||
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
|
||||
import type { ResetPasswordRequest } from '../../types/userTypes';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { resetPassword } from '../../api/authorizationAPI';
|
||||
|
||||
export interface ChangePasswordProps {
|
||||
onEditInfo: () => void;
|
||||
onPasswordChanged: () => void;
|
||||
forgettedPasswordInfo: string;
|
||||
infoType: AuthType;
|
||||
countryCode: CountryCode;
|
||||
}
|
||||
|
||||
export const ChangePassword = ({
|
||||
onEditInfo,
|
||||
onPasswordChanged,
|
||||
forgettedPasswordInfo,
|
||||
infoType,
|
||||
countryCode,
|
||||
}: ChangePasswordProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('authentication');
|
||||
const [passValue, setPassValue] = useState<string>('');
|
||||
const [confirmPassValue, setConfirmPassValue] = useState<string>('');
|
||||
const [inputTouched, setInputTouched] = useState<boolean>(false);
|
||||
const [confirmInputTouched, setConfirmInputTouched] =
|
||||
useState<boolean>(false);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] =
|
||||
useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const confirmInputRef = useRef<HTMLInputElement>(null);
|
||||
const [changePasswordLoading, setChangePasswordLoading] =
|
||||
useState<boolean>(false);
|
||||
const [changePasswordStatus, setChangePasswordStatus] = useState<
|
||||
'success' | 'failed'
|
||||
>();
|
||||
const [changePassAlertOpen, setChangePassAlertOpen] =
|
||||
useState<boolean>(false);
|
||||
const [changePassFailedMessage, setChangePassFailedMessage] =
|
||||
useState<string>('');
|
||||
|
||||
const passwordValidationRules = [
|
||||
{ title: t('forgetPassword.includingANumber'), validator: containsNumber },
|
||||
{ title: t('forgetPassword.atLeast8Characters'), validator: least8Chars },
|
||||
{
|
||||
title: t('forgetPassword.containsAnUppercaseAndLowercaseLetter'),
|
||||
validator: hasUpperAndLowerLetter,
|
||||
},
|
||||
{ title: t('forgetPassword.ContainsASymbol'), validator: containsSymbol },
|
||||
];
|
||||
|
||||
const handleBlur = () => {
|
||||
setInputTouched(true);
|
||||
};
|
||||
|
||||
const handleConfirmPassBlur = () => {
|
||||
setConfirmInputTouched(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!passValue || !isValidPassword(passValue)) {
|
||||
setInputTouched(true);
|
||||
inputRef.current?.focus();
|
||||
} else if (passValue !== confirmPassValue) {
|
||||
setConfirmInputTouched(true);
|
||||
confirmInputRef.current?.focus();
|
||||
} else {
|
||||
setChangePasswordLoading(true);
|
||||
|
||||
const apiRequest: ResetPasswordRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: undefined,
|
||||
newPassword: passValue,
|
||||
confirmNewPassword: confirmPassValue,
|
||||
};
|
||||
|
||||
const result = await resetPassword(apiRequest);
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (jsonRes.success) {
|
||||
setChangePasswordStatus('success');
|
||||
onPasswordChanged();
|
||||
} else {
|
||||
setChangePasswordStatus('failed');
|
||||
setChangePassFailedMessage(jsonRes.message);
|
||||
}
|
||||
setChangePassAlertOpen(true);
|
||||
|
||||
setChangePasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isValidPassword = (value: string) => {
|
||||
return (
|
||||
containsNumber(value) &&
|
||||
containsSymbol(value) &&
|
||||
least8Chars(value) &&
|
||||
hasUpperAndLowerLetter(value)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={changePassAlertOpen}
|
||||
onClose={() => setChangePassAlertOpen(false)}
|
||||
color={changePasswordStatus === 'failed' ? 'error' : 'success'}
|
||||
>
|
||||
{changePasswordStatus === 'failed'
|
||||
? changePassFailedMessage
|
||||
: t('forgetPassword.passwordChangedSuccessfully')}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.changePassword')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{forgettedPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1, mb: 2 }}>
|
||||
{t('forgetPassword.createANewPassword')}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label={t('forgetPassword.newPassword')}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={passValue}
|
||||
inputRef={inputRef}
|
||||
onChange={(e) => setPassValue(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
error={inputTouched && !isValidPassword(passValue)}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
|
||||
input: {
|
||||
startAdornment: confirmPassValue &&
|
||||
isValidPassword(passValue) &&
|
||||
passValue === confirmPassValue && (
|
||||
<TickCircle variant="Bold" color={theme.palette.success.main} />
|
||||
),
|
||||
endAdornment: passValue ? (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <Eye /> : <EyeSlash />}
|
||||
</IconButton>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ mt: 4 }}
|
||||
/>
|
||||
|
||||
{!isValidPassword(passValue) && (
|
||||
<Stack spacing={1} sx={{ mt: 2 }}>
|
||||
{passwordValidationRules.map((rule) => (
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<TickCircle
|
||||
variant={rule.validator(passValue) ? 'Bold' : 'Linear'}
|
||||
color={
|
||||
rule.validator(passValue)
|
||||
? theme.palette.success.main
|
||||
: theme.palette.primary.light
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography variant="body2">{rule.title}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={t('forgetPassword.confirmPassword')}
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={confirmPassValue}
|
||||
inputRef={confirmInputRef}
|
||||
onChange={(e) => setConfirmPassValue(e.target.value)}
|
||||
onBlur={handleConfirmPassBlur}
|
||||
error={confirmInputTouched && confirmPassValue !== passValue}
|
||||
slotProps={{
|
||||
htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
|
||||
input: {
|
||||
startAdornment: confirmPassValue &&
|
||||
isValidPassword(passValue) &&
|
||||
passValue === confirmPassValue && (
|
||||
<TickCircle variant="Bold" color={theme.palette.success.main} />
|
||||
),
|
||||
endAdornment: confirmPassValue ? (
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
>
|
||||
{showConfirmPassword ? <Eye /> : <EyeSlash />}
|
||||
</IconButton>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<Button loading={changePasswordLoading} onClick={handleSubmit}>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from 'react';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
|
||||
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
|
||||
export const ForgetPasswordContainer = () => {
|
||||
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
|
||||
'enterInfo' | 'verifyOtp' | 'setPassword'
|
||||
>('enterInfo');
|
||||
const [forgettedPasswordInfo, setForgettedPasswordInfo] =
|
||||
useState<string>('');
|
||||
const [infoCountryCode, setInfoCountryCode] = useState<CountryCode>('+98');
|
||||
const [infoType, setInfoType] = useState<AuthType>('email');
|
||||
|
||||
const handleVerifyOtp = () => {
|
||||
setForgetPassCurrentStep('verifyOtp');
|
||||
};
|
||||
|
||||
const handleEditInfo = () => {
|
||||
setForgetPassCurrentStep('enterInfo');
|
||||
};
|
||||
|
||||
const handleOtpVerified = () => {
|
||||
setForgetPassCurrentStep('setPassword');
|
||||
};
|
||||
|
||||
const handlePasswordChanged = () => {
|
||||
console.log('changingPasswordTo');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{forgetPassCurrentStep === 'enterInfo' && (
|
||||
<ForgettedPasswordInfo
|
||||
infoType={infoType}
|
||||
setInfoType={setInfoType}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
setForgettedPasswordInfo={setForgettedPasswordInfo}
|
||||
onVerifyOtp={handleVerifyOtp}
|
||||
countryCode={infoCountryCode}
|
||||
setCountryCode={setInfoCountryCode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{forgetPassCurrentStep === 'verifyOtp' && (
|
||||
<ForgetPasswordOtp
|
||||
countryCode={infoCountryCode}
|
||||
infoType={infoType}
|
||||
onEditInfo={handleEditInfo}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
onOTPVerified={handleOtpVerified}
|
||||
/>
|
||||
)}
|
||||
|
||||
{forgetPassCurrentStep === 'setPassword' && (
|
||||
<ChangePassword
|
||||
onEditInfo={handleEditInfo}
|
||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
||||
onPasswordChanged={handlePasswordChanged}
|
||||
infoType={infoType}
|
||||
countryCode={infoCountryCode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
import { Edit2 } from 'iconsax-react';
|
||||
import DigitInput from '@/components/components/DigitsInput';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import type { ConfirmForgetPassCodeRequest } from '../../types/userTypes';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { confirmForgetPassCode } from '../../api/authorizationAPI';
|
||||
|
||||
interface ForgetPasswordOtpProps {
|
||||
forgettedPasswordInfo: string;
|
||||
infoType: AuthType;
|
||||
countryCode: CountryCode;
|
||||
onEditInfo: () => void;
|
||||
onOTPVerified: (otpCode: string) => void;
|
||||
}
|
||||
|
||||
export function ForgetPasswordOtp({
|
||||
forgettedPasswordInfo,
|
||||
infoType,
|
||||
countryCode,
|
||||
onEditInfo,
|
||||
onOTPVerified,
|
||||
}: ForgetPasswordOtpProps) {
|
||||
const [otpCode, setOtpCode] = useState<string>('');
|
||||
const [otpDigitInvalid, setOtpDigitInvalid] = useState<boolean>(false);
|
||||
const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>();
|
||||
const [verifyStatusLoading, setVerifyStatusLoading] =
|
||||
useState<boolean>(false);
|
||||
const [verifyAlertMessage, setVerifyAlertMessage] = useState<string>();
|
||||
const { t } = useTranslation('authentication');
|
||||
const [resendTimer, setResendTimer] = useState<number>(120);
|
||||
const [canResend, setCanResend] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
if (resendTimer > 0) {
|
||||
interval = setInterval(() => {
|
||||
setResendTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
setCanResend(true);
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [resendTimer]);
|
||||
|
||||
const handleResendOTPCode = () => {
|
||||
setResendLoading(true);
|
||||
|
||||
// TODO: Call API here instead of settimeout
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('resended');
|
||||
|
||||
setResendTimer(120);
|
||||
setCanResend(false);
|
||||
setResendLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const min = Math.floor(seconds / 60);
|
||||
const sec = seconds % 60;
|
||||
return `${min}:${sec.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const handleVerifyOTP = async () => {
|
||||
if (!otpCode || otpCode.length < 4) {
|
||||
setOtpDigitInvalid(true);
|
||||
} else {
|
||||
setOtpDigitInvalid(false);
|
||||
setVerifyStatusLoading(true);
|
||||
|
||||
// Change setTimeout to api call
|
||||
const apiRequest: ConfirmForgetPassCodeRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: undefined,
|
||||
code: otpCode,
|
||||
};
|
||||
|
||||
const result = await confirmForgetPassCode(apiRequest);
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (jsonRes.success) {
|
||||
setVerifyStatus('success');
|
||||
onOTPVerified(otpCode);
|
||||
} else {
|
||||
setVerifyStatus('failed');
|
||||
setVerifyAlertMessage(jsonRes.message);
|
||||
}
|
||||
|
||||
setVerifyStatusLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack alignItems="center">
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
open={!!verifyAlertMessage}
|
||||
onClose={() => setVerifyAlertMessage(undefined)}
|
||||
color={'error'}
|
||||
>
|
||||
{verifyAlertMessage}
|
||||
</Toast>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 4,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{ textTransform: 'lowercase', width: 'auto' }}
|
||||
endIcon={<Edit2 />}
|
||||
onClick={onEditInfo}
|
||||
>
|
||||
{infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: forgettedPasswordInfo}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
|
||||
{infoType === 'email'
|
||||
? t(
|
||||
'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
|
||||
)
|
||||
: t(
|
||||
'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<DigitInput
|
||||
error={otpDigitInvalid || verifyStatus === 'failed'}
|
||||
success={verifyStatus === 'success'}
|
||||
onChange={(value) => setOtpCode(value)}
|
||||
/>
|
||||
|
||||
<Button onClick={handleVerifyOTP} loading={verifyStatusLoading}>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</AuthenticationCard>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
mt: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{t('verify.resendCodeIn')}</Typography>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
loading={resendLoading}
|
||||
sx={{ width: 'auto' }}
|
||||
onClick={canResend ? handleResendOTPCode : undefined}
|
||||
>
|
||||
{canResend && t('verify.resendCode')}
|
||||
{!canResend && `${formatTime(resendTimer)} ${t('verify.moreMinute')}`}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Button, Stack, TextField, Typography } from '@mui/material';
|
||||
import { useRef, useState, type Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import type { AuthType } from '../../types/authTypes';
|
||||
import { isEmail } from '@/utils/regexes/isEmail';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { CountryCodeSelector } from '../CountryCodeSelector';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { sendForgetPassCode } from '../../api/authorizationAPI';
|
||||
import type { SendForgetPassCodeRequest } from '../../types/userTypes';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
|
||||
|
||||
export interface ForgettedPasswordInfoProps {
|
||||
forgettedPasswordInfo: string;
|
||||
setForgettedPasswordInfo: Dispatch<string>;
|
||||
infoType: AuthType;
|
||||
setInfoType: Dispatch<AuthType>;
|
||||
onVerifyOtp: (value: string) => void;
|
||||
countryCode: CountryCode;
|
||||
setCountryCode: Dispatch<CountryCode>;
|
||||
}
|
||||
|
||||
export function ForgettedPasswordInfo({
|
||||
forgettedPasswordInfo,
|
||||
setForgettedPasswordInfo,
|
||||
infoType,
|
||||
setInfoType,
|
||||
onVerifyOtp,
|
||||
countryCode,
|
||||
setCountryCode,
|
||||
}: ForgettedPasswordInfoProps) {
|
||||
const { t } = useTranslation('authentication');
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [error, setError] = useState<string>();
|
||||
const [touched, setTouched] = useState<boolean>(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [sendCodeLoading, setSendCodeLoading] = useState<boolean>(false);
|
||||
const inputError: boolean = touched && !!error;
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
setForgettedPasswordInfo(newValue);
|
||||
|
||||
// If the new value contains only digits (or is empty), it's a phone number
|
||||
if (isNumeric(newValue)) {
|
||||
setInfoType('phone');
|
||||
} else {
|
||||
setInfoType('email');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
validateInput(forgettedPasswordInfo, infoType);
|
||||
};
|
||||
|
||||
const validateInput = (
|
||||
value: string,
|
||||
authType: AuthType,
|
||||
setErrors: boolean = true,
|
||||
) => {
|
||||
if (!value) {
|
||||
if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
|
||||
return false;
|
||||
} else if (authType === 'email' && !isEmail(value)) {
|
||||
if (setErrors) setError(t('loginForm.emailIsInvalid'));
|
||||
return false;
|
||||
} else if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
|
||||
if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
|
||||
return false;
|
||||
} else {
|
||||
if (setErrors) setError(undefined);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (validateInput(forgettedPasswordInfo, infoType, false)) {
|
||||
setSendCodeLoading(true);
|
||||
|
||||
const sendCodeRequest: SendForgetPassCodeRequest = {
|
||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
||||
phoneNumber:
|
||||
infoType === 'phone'
|
||||
? countryCode + forgettedPasswordInfo
|
||||
: undefined,
|
||||
};
|
||||
const result = await sendForgetPassCode(sendCodeRequest);
|
||||
const jsonRes = await result.json();
|
||||
|
||||
if (!jsonRes.success) {
|
||||
setErrorMessage(jsonRes.message);
|
||||
}
|
||||
|
||||
setSendCodeLoading(false);
|
||||
onVerifyOtp(forgettedPasswordInfo);
|
||||
} else {
|
||||
inputRef.current?.focus();
|
||||
validateInput(forgettedPasswordInfo, infoType);
|
||||
}
|
||||
};
|
||||
|
||||
const showAdornment =
|
||||
infoType === 'phone' && forgettedPasswordInfo.length > 0;
|
||||
|
||||
return (
|
||||
<AuthenticationCard>
|
||||
<Toast
|
||||
color="error"
|
||||
onClose={() => setErrorMessage(undefined)}
|
||||
open={!!errorMessage}
|
||||
>
|
||||
{errorMessage}
|
||||
</Toast>
|
||||
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h5">
|
||||
{t('forgetPassword.forgetPassword')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t(
|
||||
'forgetPassword.pleaseEnterYourMobileNumberEmailToRecoverYourPassword',
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
label={t('loginForm.emailOrPhoneLabel')}
|
||||
value={forgettedPasswordInfo}
|
||||
onChange={handleInputChange}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
autoFocus
|
||||
slotProps={{
|
||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
show={showAdornment}
|
||||
menuAnchor={textFieldRef.current}
|
||||
onCloseFocusRef={inputRef}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
sx={{ my: 4, mb: 8 }}
|
||||
/>
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Button loading={sendCodeLoading} onClick={handleSubmit}>
|
||||
{t('forgetPassword.confirm')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</AuthenticationCard>
|
||||
);
|
||||
}
|
||||
0
src/features/authorization/index.ts
Normal file
0
src/features/authorization/index.ts
Normal file
20
src/features/authorization/routes/AuthenticationPage.tsx
Normal file
20
src/features/authorization/routes/AuthenticationPage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FlexBox } from '@/components/components/common/FlexBox';
|
||||
import Logo from '@/components/Logo';
|
||||
import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps';
|
||||
|
||||
export function AuthenticationPage() {
|
||||
return (
|
||||
<FlexBox
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
<AuthenticationSteps />
|
||||
</FlexBox>
|
||||
);
|
||||
}
|
||||
20
src/features/authorization/routes/ForgetPasswordPage.tsx
Normal file
20
src/features/authorization/routes/ForgetPasswordPage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FlexBox } from '@/components/components/common/FlexBox';
|
||||
import Logo from '@/components/Logo';
|
||||
import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer';
|
||||
|
||||
export function ForgetPasswordPage() {
|
||||
return (
|
||||
<FlexBox
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
gap: 3,
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
<ForgetPasswordContainer />
|
||||
</FlexBox>
|
||||
);
|
||||
}
|
||||
10
src/features/authorization/types/authTypes.ts
Normal file
10
src/features/authorization/types/authTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type AuthType = 'email' | 'phone';
|
||||
|
||||
export type AuthMode = 'register' | 'login';
|
||||
|
||||
export type AuthStep =
|
||||
| 'emailOrPhone'
|
||||
| 'verify'
|
||||
| 'enterPassword'
|
||||
| 'addPhoneNumber'
|
||||
| 'addedPhoneNumberVerify';
|
||||
138
src/features/authorization/types/userTypes.ts
Normal file
138
src/features/authorization/types/userTypes.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// GetUserStatusByPhoneNumberOrEmail
|
||||
|
||||
import type { ApiResponse } from '@/types/apiResponse';
|
||||
import type { GUID } from '@/types/commonTypes';
|
||||
|
||||
export interface GetUserStatusByPhoneNumberOrEmailRequest {
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse {
|
||||
userStatus: UserStatus;
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
None = 0,
|
||||
RegisteredWithPassword = 1,
|
||||
RegisteredWithoutPassword = 2,
|
||||
NotRegistered = 3,
|
||||
}
|
||||
|
||||
// LoginOrSignUpWithOtp
|
||||
|
||||
export interface LoginRequest {
|
||||
otpCode: string;
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
export interface PasswordLoginRequest {
|
||||
phoneNumber?: string;
|
||||
email?: string;
|
||||
password: string;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse extends ApiResponse {
|
||||
returnUrl: string;
|
||||
userId: GUID;
|
||||
registeredWithOutPhoneNumber: boolean;
|
||||
completedUserInformation: boolean;
|
||||
}
|
||||
|
||||
// SendSmsOtp
|
||||
|
||||
export interface SendSmsOtpRequest {
|
||||
phoneNumber: string;
|
||||
}
|
||||
|
||||
// SendEmailOtp
|
||||
|
||||
export interface SendEmailOtpRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
// ConfirmOtp
|
||||
|
||||
export interface ConfirmEmailOtpRequest {
|
||||
email: string;
|
||||
otpCode: string;
|
||||
}
|
||||
|
||||
export interface ConfirmSmsOtpRequest {
|
||||
phoneNumber: string;
|
||||
otpCode: string;
|
||||
}
|
||||
|
||||
export interface ConfirmOtpResponse extends ApiResponse {
|
||||
confirm: boolean;
|
||||
}
|
||||
|
||||
// ResetPassword
|
||||
|
||||
export interface ResetPasswordRequest {
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
newPassword: string;
|
||||
confirmNewPassword: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordResponse extends ApiResponse {
|
||||
passwordChanged: boolean;
|
||||
}
|
||||
|
||||
// SendForgetPassCode
|
||||
|
||||
export interface SendForgetPassCodeRequest {
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
}
|
||||
|
||||
// ConfirmForgetPassCode
|
||||
|
||||
export interface ConfirmForgetPassCodeRequest {
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
// LoginOrSignUpWithGoogle
|
||||
|
||||
export interface GoogleCodeClientResponse {
|
||||
id_token: string;
|
||||
}
|
||||
|
||||
export interface LoginOrSignUpWithGoogleRequest {
|
||||
idToken: string;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
export interface LoginOrSignUpWithGoogleResponse extends ApiResponse {
|
||||
userId: GUID;
|
||||
registeredWithOutPhoneNumber: boolean;
|
||||
completedUserInformation: boolean;
|
||||
returnUrl: string;
|
||||
}
|
||||
|
||||
// CompleteUserInformation
|
||||
|
||||
export interface CompleteUserInformationRequest {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
gender?: Gender;
|
||||
nationalCode?: string;
|
||||
savePassword?: boolean;
|
||||
password?: string;
|
||||
saveEmail?: boolean;
|
||||
email?: string;
|
||||
birthDate?: string;
|
||||
countryCode?: string;
|
||||
userId?: GUID;
|
||||
}
|
||||
|
||||
export enum Gender {
|
||||
Male = 1,
|
||||
Female = 2,
|
||||
}
|
||||
12
src/global.d.ts
vendored
Normal file
12
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface Element extends React.ReactElement<any, any> {}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
google: typeof google;
|
||||
}
|
||||
const google: any;
|
||||
}
|
||||
@@ -25,13 +25,28 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
cssVariables: {
|
||||
colorSchemeSelector: 'class',
|
||||
},
|
||||
components: {
|
||||
MuiTextField: {
|
||||
defaultProps: {
|
||||
variant: 'outlined',
|
||||
fullWidth: true,
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
size: 'large',
|
||||
fullWidth: true,
|
||||
variant: 'contained',
|
||||
},
|
||||
},
|
||||
},
|
||||
spacing: 8,
|
||||
typography: typography,
|
||||
});
|
||||
}, [i18n]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme} defaultMode="system">
|
||||
<ThemeProvider theme={theme} defaultMode="light">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
@@ -10,17 +10,14 @@ export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { i18n } = useTranslation();
|
||||
const [cache, setCache] = useState(createCache({ key: 'css' }));
|
||||
|
||||
useEffect(() => {
|
||||
const newDir = i18n.dir(i18n.language);
|
||||
|
||||
const newCache = createCache({
|
||||
key: 'mui',
|
||||
stylisPlugins: newDir === 'rtl' ? [prefixer, rtlPlugin] : [],
|
||||
const cacheRtl = useMemo(() => {
|
||||
const isRtl = i18n.dir(i18n.language) === 'rtl';
|
||||
return createCache({
|
||||
key: isRtl ? 'muirtl' : 'muiltr',
|
||||
stylisPlugins: isRtl ? [prefixer, rtlPlugin] : [],
|
||||
});
|
||||
setCache(newCache);
|
||||
}, [i18n, i18n.language]);
|
||||
}, [i18n]);
|
||||
|
||||
return <CacheProvider value={cache}>{children}</CacheProvider>;
|
||||
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
|
||||
};
|
||||
|
||||
@@ -49,9 +49,9 @@ export const PALETTE: Palette = {
|
||||
},
|
||||
error: {
|
||||
light: {
|
||||
main: '#E53935',
|
||||
dark: '#C62828',
|
||||
light: '#EF5350',
|
||||
main: '#d32f2f',
|
||||
dark: '#c62828',
|
||||
light: '#ef5350',
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
dark: {
|
||||
@@ -91,9 +91,9 @@ export const PALETTE: Palette = {
|
||||
},
|
||||
success: {
|
||||
light: {
|
||||
main: '#43A047',
|
||||
dark: '#1B5E20',
|
||||
light: '#81C784',
|
||||
main: '#2e7d32',
|
||||
dark: '#1b5e20',
|
||||
light: '#4caf50',
|
||||
contrastText: '#FFFFFF',
|
||||
},
|
||||
// TODO
|
||||
|
||||
13
src/types/apiResponse.ts
Normal file
13
src/types/apiResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
errorCode: number;
|
||||
message: string;
|
||||
validations: ApiResponseValidation[];
|
||||
}
|
||||
|
||||
export interface ApiResponseValidation {
|
||||
message: string;
|
||||
code: number;
|
||||
property: string;
|
||||
severity: number;
|
||||
}
|
||||
3
src/types/commonTypes.ts
Normal file
3
src/types/commonTypes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type GUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||
|
||||
export type CountryCode = `+${number}`;
|
||||
5
src/types/fetchPromise.ts
Normal file
5
src/types/fetchPromise.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type FetchPromise<T = ''> = Promise<FetchResponse<T>>;
|
||||
|
||||
export interface FetchResponse<T> extends Response {
|
||||
json(): Promise<T>;
|
||||
}
|
||||
1
src/utils/regexes/containsNumber.tsx
Normal file
1
src/utils/regexes/containsNumber.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const containsNumber = (value: string) => /\d/.test(value);
|
||||
1
src/utils/regexes/containsSymbol.tsx
Normal file
1
src/utils/regexes/containsSymbol.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const containsSymbol = (value: string) => /[!@#$%&*\^]/.test(value);
|
||||
5
src/utils/regexes/hasUpperAndLowerLetter.tsx
Normal file
5
src/utils/regexes/hasUpperAndLowerLetter.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export const hasUpperAndLowerLetter = (value: string) => {
|
||||
const hasUpper = /[A-Z]/.test(value);
|
||||
const hasLower = /[a-z]/.test(value);
|
||||
return hasUpper && hasLower;
|
||||
};
|
||||
2
src/utils/regexes/isEmail.tsx
Normal file
2
src/utils/regexes/isEmail.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export const isEmail = (value: string) =>
|
||||
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
|
||||
1
src/utils/regexes/isNumeric.ts
Normal file
1
src/utils/regexes/isNumeric.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isNumeric = (value: string) => /^\d*$/.test(value);
|
||||
7
src/utils/regexes/isValidPhoneNumber.tsx
Normal file
7
src/utils/regexes/isValidPhoneNumber.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
|
||||
export const isPhoneNumber = (code: string, phone: string) => {
|
||||
const phoneNumber = parsePhoneNumberFromString(code + phone);
|
||||
|
||||
return phoneNumber && phoneNumber.isValid();
|
||||
};
|
||||
1
src/utils/regexes/least8Chars.tsx
Normal file
1
src/utils/regexes/least8Chars.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export const least8Chars = (value: string) => value.length >= 8;
|
||||
2
vite.config.d.ts
vendored
Normal file
2
vite.config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const _default: import("vite").UserConfig;
|
||||
export default _default;
|
||||
12
vite.config.js
Normal file
12
vite.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user