From 62747f6ca8292f82e50b6d401602ac12efd8ddba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=85=D9=87=D8=B1=D8=B2=D8=A7=D8=AF=20=D9=82=D8=AF=D8=B1?= =?UTF-8?q?=D8=AA=DB=8C?= Date: Sat, 26 Jul 2025 13:42:18 +0330 Subject: [PATCH] chore: phone number validation added --- package-lock.json | 7 + package.json | 1 + public/locales/en/common.json | 249 +++++++++++++++++- public/locales/fa/common.json | 183 ++++++++++++- .../components/CountryCodeSelector.tsx | 3 +- .../authentication/components/LoginForm.tsx | 90 ------- .../components/LoginRegiserForm.tsx | 24 +- 7 files changed, 451 insertions(+), 106 deletions(-) delete mode 100644 src/features/authentication/components/LoginForm.tsx diff --git a/package-lock.json b/package-lock.json index eb709eb..9e9436f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 22e1fc7..91e1810 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index bc4b6ab..72b36d8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,3 +1,250 @@ { - "helloWorld": "hello world" + "helloWorld": "hello world", + "country": { + "afghanistan": "Afghanistan", + "aland_islands": "Aland islands", + "albania": "Albania", + "algeria": "Algeria", + "american_samoa": "American samoa", + "andorra": "Andorra", + "angola": "Angola", + "anguilla": "Anguilla", + "antarctica": "Antarctica", + "antigua_and_barbuda": "Antigua and barbuda", + "argentina": "Argentina", + "armenia": "Armenia", + "aruba": "Aruba", + "australia": "Australia", + "austria": "Austria", + "azerbaijan": "Azerbaijan", + "bahamas": "Bahamas", + "bahrain": "Bahrain", + "bangladesh": "Bangladesh", + "barbados": "Barbados", + "belarus": "Belarus", + "belgium": "Belgium", + "belize": "Belize", + "benin": "Benin", + "bermuda": "Bermuda", + "bhutan": "Bhutan", + "bolivia": "Bolivia", + "bosnia_and_herzegovina": "Bosnia and herzegovina", + "botswana": "Botswana", + "brazil": "Brazil", + "british_indian_ocean_territory": "British indian ocean territory", + "british_virgin_islands": "British virgin islands", + "brunei": "Brunei", + "bulgaria": "Bulgaria", + "burkina_faso": "Burkina faso", + "burundi": "Burundi", + "cambodia": "Cambodia", + "cameroon": "Cameroon", + "canada": "Canada", + "cape_verde": "Cape verde", + "cayman_islands": "Cayman islands", + "central_african_republic": "Central african republic", + "chad": "Chad", + "chile": "Chile", + "china": "China", + "christmas_island": "Christmas island", + "cocos_keeling_islands": "Cocos keeling islands", + "colombia": "Colombia", + "comoros": "Comoros", + "congo_brazzaville": "Congo brazzaville", + "congo_kinshasa": "Congo kinshasa", + "cook_islands": "Cook islands", + "costa_rica": "Costa rica", + "cote_divoire": "Cote divoire", + "croatia": "Croatia", + "cuba": "Cuba", + "curacao": "Curacao", + "cyprus": "Cyprus", + "czech_republic": "Czech republic", + "denmark": "Denmark", + "djibouti": "Djibouti", + "dominica": "Dominica", + "dominican_republic": "Dominican republic", + "ecuador": "Ecuador", + "egypt": "Egypt", + "el_salvador": "El salvador", + "equatorial_guinea": "Equatorial guinea", + "eritrea": "Eritrea", + "estonia": "Estonia", + "eswatini": "Eswatini", + "ethiopia": "Ethiopia", + "falkland_islands": "Falkland islands", + "faroe_islands": "Faroe islands", + "fiji": "Fiji", + "finland": "Finland", + "france": "France", + "french_guiana": "French guiana", + "french_polynesia": "French polynesia", + "gabon": "Gabon", + "gambia": "Gambia", + "georgia": "Georgia", + "germany": "Germany", + "ghana": "Ghana", + "gibraltar": "Gibraltar", + "greece": "Greece", + "greenland": "Greenland", + "grenada": "Grenada", + "guadeloupe": "Guadeloupe", + "guam": "Guam", + "guatemala": "Guatemala", + "guernsey": "Guernsey", + "guinea": "Guinea", + "guinea_bissau": "Guinea bissau", + "guyana": "Guyana", + "haiti": "Haiti", + "honduras": "Honduras", + "hong_kong": "Hong kong", + "hungary": "Hungary", + "iceland": "Iceland", + "india": "India", + "indonesia": "Indonesia", + "iran": "Iran", + "iraq": "Iraq", + "ireland": "Ireland", + "isle_of_man": "Isle of man", + "israel": "Israel", + "italy": "Italy", + "jamaica": "Jamaica", + "japan": "Japan", + "jersey": "Jersey", + "jordan": "Jordan", + "kazakhstan": "Kazakhstan", + "kenya": "Kenya", + "kiribati": "Kiribati", + "kosovo": "Kosovo", + "kuwait": "Kuwait", + "kyrgyzstan": "Kyrgyzstan", + "laos": "Laos", + "latvia": "Latvia", + "lebanon": "Lebanon", + "lesotho": "Lesotho", + "liberia": "Liberia", + "libya": "Libya", + "liechtenstein": "Liechtenstein", + "lithuania": "Lithuania", + "luxembourg": "Luxembourg", + "macau": "Macau", + "madagascar": "Madagascar", + "malawi": "Malawi", + "malaysia": "Malaysia", + "maldives": "Maldives", + "mali": "Mali", + "malta": "Malta", + "marshall_islands": "Marshall islands", + "martinique": "Martinique", + "mauritania": "Mauritania", + "mauritius": "Mauritius", + "mayotte": "Mayotte", + "mexico": "Mexico", + "micronesia": "Micronesia", + "moldova": "Moldova", + "monaco": "Monaco", + "mongolia": "Mongolia", + "montenegro": "Montenegro", + "montserrat": "Montserrat", + "morocco": "Morocco", + "mozambique": "Mozambique", + "myanmar": "Myanmar", + "namibia": "Namibia", + "nauru": "Nauru", + "nepal": "Nepal", + "netherlands": "Netherlands", + "new_caledonia": "New caledonia", + "new_zealand": "New zealand", + "nicaragua": "Nicaragua", + "niger": "Niger", + "nigeria": "Nigeria", + "niue": "Niue", + "norfolk_island": "Norfolk island", + "north_korea": "North korea", + "north_macedonia": "North macedonia", + "northern_mariana_islands": "Northern mariana islands", + "norway": "Norway", + "oman": "Oman", + "pakistan": "Pakistan", + "palau": "Palau", + "palestine": "Palestine", + "panama": "Panama", + "papua_new_guinea": "Papua new guinea", + "paraguay": "Paraguay", + "peru": "Peru", + "philippines": "Philippines", + "pitcairn_islands": "Pitcairn islands", + "poland": "Poland", + "portugal": "Portugal", + "puerto_rico": "Puerto rico", + "qatar": "Qatar", + "reunion": "Reunion", + "romania": "Romania", + "russia": "Russia", + "rwanda": "Rwanda", + "saint_barthelemy": "Saint barthelemy", + "saint_helena": "Saint helena", + "saint_kitts_and_nevis": "Saint kitts and nevis", + "saint_lucia": "Saint lucia", + "saint_martin": "Saint martin", + "saint_pierre_and_miquelon": "Saint pierre and miquelon", + "saint_vincent_and_the_grenadines": "Saint vincent and the grenadines", + "samoa": "Samoa", + "san_marino": "San marino", + "sao_tome_and_principe": "Sao tome and principe", + "saudi_arabia": "Saudi arabia", + "senegal": "Senegal", + "serbia": "Serbia", + "seychelles": "Seychelles", + "sierra_leone": "Sierra leone", + "singapore": "Singapore", + "sint_maarten": "Sint maarten", + "slovakia": "Slovakia", + "slovenia": "Slovenia", + "solomon_islands": "Solomon islands", + "somalia": "Somalia", + "south_africa": "South africa", + "south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands", + "south_korea": "South korea", + "south_sudan": "South sudan", + "spain": "Spain", + "sri_lanka": "Sri lanka", + "sudan": "Sudan", + "suriname": "Suriname", + "svalbard_and_jan_mayen": "Svalbard and jan mayen", + "sweden": "Sweden", + "switzerland": "Switzerland", + "syria": "Syria", + "taiwan": "Taiwan", + "tajikistan": "Tajikistan", + "tanzania": "Tanzania", + "thailand": "Thailand", + "timor_leste": "Timor leste", + "togo": "Togo", + "tokelau": "Tokelau", + "tonga": "Tonga", + "trinidad_and_tobago": "Trinidad and tobago", + "tunisia": "Tunisia", + "turkey": "Turkey", + "turkmenistan": "Turkmenistan", + "turks_and_caicos_islands": "Turks and caicos islands", + "tuvalu": "Tuvalu", + "us_virgin_islands": "Us virgin islands", + "uganda": "Uganda", + "ukraine": "Ukraine", + "united_arab_emirates": "United arab emirates", + "united_kingdom": "United kingdom", + "united_states": "United states", + "uruguay": "Uruguay", + "uzbekistan": "Uzbekistan", + "vanuatu": "Vanuatu", + "vatican_city": "Vatican city", + "venezuela": "Venezuela", + "vietnam": "Vietnam", + "wallis_and_futuna": "Wallis and futuna", + "western_sahara": "Western sahara", + "yemen": "Yemen", + "zambia": "Zambia", + "zimbabwe": "Zimbabwe" + } } diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json index eb50682..71ba38f 100644 --- a/public/locales/fa/common.json +++ b/public/locales/fa/common.json @@ -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": "نتیجه ای یافت نشد." } diff --git a/src/features/authentication/components/CountryCodeSelector.tsx b/src/features/authentication/components/CountryCodeSelector.tsx index ea4d89d..6028d6b 100644 --- a/src/features/authentication/components/CountryCodeSelector.tsx +++ b/src/features/authentication/components/CountryCodeSelector.tsx @@ -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({ }} /> - + {country.phone} diff --git a/src/features/authentication/components/LoginForm.tsx b/src/features/authentication/components/LoginForm.tsx deleted file mode 100644 index 0584f23..0000000 --- a/src/features/authentication/components/LoginForm.tsx +++ /dev/null @@ -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(null); - const inputRef = useRef(null); - const dir = i18n.dir(); - - const handleInputChange = (event: React.ChangeEvent) => { - 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 ( - - - {t('loginForm.title')} - - {t('loginForm.description')} - - - - - ), - endAdornment: dir === 'rtl' && ( - - ), - }, - }} - sx={{ my: 4 }} - /> - - - - - - - ); -} diff --git a/src/features/authentication/components/LoginRegiserForm.tsx b/src/features/authentication/components/LoginRegiserForm.tsx index c399826..233eb66 100644 --- a/src/features/authentication/components/LoginRegiserForm.tsx +++ b/src/features/authentication/components/LoginRegiserForm.tsx @@ -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' && ( - - ), - endAdornment: dir === 'rtl' && ( + endAdornment: (