chore: phone number validation added

This commit is contained in:
مهرزاد قدرتی
2025-07-26 13:42:18 +03:30
parent 2e10a5496c
commit 62747f6ca8
7 changed files with 451 additions and 106 deletions

7
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"iconsax-reactjs": "^0.0.8",
"libphonenumber-js": "^1.12.10",
"react": "^19.1.0",
"react-country-flag": "^3.1.0",
"react-dom": "^19.1.0",
@@ -3306,6 +3307,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.12.10",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz",
"integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==",
"license": "MIT"
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",

View File

@@ -19,6 +19,7 @@
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"iconsax-reactjs": "^0.0.8",
"libphonenumber-js": "^1.12.10",
"react": "^19.1.0",
"react-country-flag": "^3.1.0",
"react-dom": "^19.1.0",

View File

@@ -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"
}
}

View File

@@ -2,7 +2,188 @@
"labels": {
"search": "جست و جو"
},
"country"
"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": "نتیجه ای یافت نشد."
}

View File

@@ -72,6 +72,7 @@ export function CountryCodeSelector({
() =>
countries.filter(
(country) =>
t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
country.phone.includes(searchTerm),
),
@@ -205,7 +206,7 @@ export function CountryCodeSelector({
}}
/>
</ListItemIcon>
<ListItemText primary={country.label} />
<ListItemText primary={t(country.label)} />
<Typography color="text.secondary">
{country.phone}
</Typography>

View File

@@ -1,90 +0,0 @@
import { Box, Button, Stack, TextField, Typography } from '@mui/material';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CountryCodeSelector } from './CountryCodeSelector';
import { Google } from 'iconsax-reactjs';
import { isNumeric } from '@/utils/regexes/isNumeric';
export function LoginForm() {
const { t, i18n } = useTranslation('authentication');
const [value, setValue] = useState('');
const [countryCode, setCountryCode] = useState('+41');
const [inputType, setInputType] = useState<'phone' | 'email'>('phone');
const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const dir = i18n.dir();
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
setValue(newValue);
// If the new value contains only digits (or is empty), it's a phone number
if (isNumeric(newValue)) {
setInputType('phone');
} else {
setInputType('email');
}
};
const handleBlur = () => {
// setTouched(true);
// setError(validate(value, inputType));
};
const showAdornment = inputType === 'phone' && value.length > 0;
return (
<Box sx={{ width: '100%' }}>
<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={value}
onChange={handleInputChange}
onBlur={handleBlur}
// error={touched && !!error}
autoFocus
slotProps={{
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
input: {
startAdornment: dir === 'ltr' && (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
show={showAdornment}
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
),
endAdornment: dir === 'rtl' && (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
show={showAdornment}
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
),
},
}}
sx={{ my: 4 }}
/>
<Stack spacing={2}>
<Button onClick={() => inputRef.current?.focus()}>
{t('loginForm.submitButton')}
</Button>
<Button variant="outlined" startIcon={<Google variant="Bold" />}>
{t('loginForm.loginWithGoogle')}
</Button>
</Stack>
</Box>
);
}

View File

@@ -6,6 +6,7 @@ import { Google } from 'iconsax-reactjs';
import { isNumeric } from '@/utils/regexes/isNumeric';
import type { AuthMode, AuthType } from '../types/auth-types';
import { isEmail } from '@/utils/regexes/isEmail';
import parsePhoneNumberFromString from 'libphonenumber-js';
export interface LoginRegisterFormProps {
authType: AuthType;
@@ -49,13 +50,19 @@ export function LoginRegisterForm({
setError(t('loginForm.thisFieldIsRequired'));
} else if (authType === 'email' && !isEmail(value)) {
setError(t('loginForm.emailIsInvalid'));
} else if (authType === 'phone' && false /* TODO */) {
setError(t('loginForm.emailIsInvalid'));
} else if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
setError(t('loginForm.phoneNumberIsInvalid'));
} else {
setError(undefined);
}
};
const isPhoneValid = (code: string, phone: string) => {
const phoneNumber = parsePhoneNumberFromString(code + phone);
return phoneNumber && phoneNumber.isValid();
};
const isInputValid = (value: string, authType: AuthType): boolean => {
if (!value) {
return false;
@@ -65,7 +72,7 @@ export function LoginRegisterForm({
return false;
}
if (authType === 'phone' && false /* TODO */) {
if (authType === 'phone' && !isPhoneValid(countryCode, value)) {
return false;
}
@@ -104,16 +111,7 @@ export function LoginRegisterForm({
slotProps={{
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
input: {
startAdornment: dir === 'ltr' && (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
show={showAdornment}
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
),
endAdornment: dir === 'rtl' && (
endAdornment: (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}