diff --git a/package-lock.json b/package-lock.json
index 51ed5f9..f70be3c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
+ "libphonenumber-js": "^1.12.6",
"lodash": "^4.17.21",
"next": "15.1.2",
"next-auth": "^4.24.11",
@@ -45,6 +46,7 @@
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.2",
"react-perfect-scrollbar": "1.5.8",
+ "react-phone-input-2": "^2.15.1",
"react-toastify": "^10.0.6",
"react-use": "17.6.0",
"recharts": "^2.15.1",
@@ -7313,6 +7315,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.12.6",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz",
+ "integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==",
+ "license": "MIT"
+ },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -7366,6 +7374,12 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -7396,6 +7410,12 @@
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -7407,6 +7427,18 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
+ "node_modules/lodash.reduce": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
+ "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.startswith": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz",
+ "integrity": "sha512-XClYR1h4/fJ7H+mmCKppbiBmljN/nGs73iq2SjCT9SF4CBPoUHzLvWmH1GtZMhMBZSiRkHXfeA2RY1eIlJ75ww==",
+ "license": "MIT"
+ },
"node_modules/lodash.truncate": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
@@ -8840,6 +8872,24 @@
"react-dom": ">=16.3.3"
}
},
+ "node_modules/react-phone-input-2": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/react-phone-input-2/-/react-phone-input-2-2.15.1.tgz",
+ "integrity": "sha512-W03abwhXcwUoq+vUFvC6ch2+LJYMN8qSOiO889UH6S7SyMCQvox/LF3QWt+cZagZrRdi5z2ON3omnjoCUmlaYw==",
+ "license": "MIT",
+ "dependencies": {
+ "classnames": "^2.2.6",
+ "lodash.debounce": "^4.0.8",
+ "lodash.memoize": "^4.1.2",
+ "lodash.reduce": "^4.6.0",
+ "lodash.startswith": "^4.2.1",
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
+ "react-dom": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0"
+ }
+ },
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
diff --git a/package.json b/package.json
index a958de0..cf6534f 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
+ "libphonenumber-js": "^1.12.6",
"lodash": "^4.17.21",
"next": "15.1.2",
"next-auth": "^4.24.11",
@@ -52,6 +53,7 @@
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.2",
"react-perfect-scrollbar": "1.5.8",
+ "react-phone-input-2": "^2.15.1",
"react-toastify": "^10.0.6",
"react-use": "17.6.0",
"recharts": "^2.15.1",
diff --git a/src/@core/svg/Logo.tsx b/src/@core/svg/Logo.tsx
index da7e24c..0e83694 100644
--- a/src/@core/svg/Logo.tsx
+++ b/src/@core/svg/Logo.tsx
@@ -1,7 +1,7 @@
'use client'
import Image from 'next/image'
-
+import Link from 'next/link'
import { useSettings } from '../hooks/useSettings'
const Logo = ({ className }: { className?: string }) => {
@@ -9,13 +9,17 @@ const Logo = ({ className }: { className?: string }) => {
return (
-
+
+
+
)
}
diff --git a/src/components/PhoneInput.tsx b/src/components/PhoneInput.tsx
new file mode 100644
index 0000000..2577866
--- /dev/null
+++ b/src/components/PhoneInput.tsx
@@ -0,0 +1,185 @@
+'use client'
+
+import React, { useEffect,useState } from 'react';
+
+import PhoneInput from 'react-phone-input-2';
+import 'react-phone-input-2/lib/style.css'
+import { useTheme } from '@mui/material/styles';
+import { Typography, Box } from '@mui/material';
+
+interface ThemedPhoneInputProps {
+ value?: string;
+ onChange: (value: string, data: any) => void;
+ onBlur?: (e: React.FocusEvent) => void;
+ onFocus?: (e: React.FocusEvent) => void;
+ error?: boolean;
+ helperText?: string;
+ label?: string;
+ required?: boolean;
+ name?: string;
+ country?: string;
+}
+
+const ThemedPhoneInput: React.FC = ({
+ value,
+ onChange,
+ onBlur,
+ onFocus,
+ error,
+ helperText,
+ label,
+ required = false,
+ name = 'phoneNumber',
+ country = 'gb'
+}) => {
+ const theme = useTheme();
+ const [isFocused, setIsFocused] = useState(false);
+
+ useEffect(() => {
+ const applyHoverStyles = () => {
+ const style = document.createElement('style');
+
+ style.innerHTML = `
+ .react-tel-input .country:hover {
+ background-color: rgba(110, 65, 226, 0.15) !important;
+ color: #8f6ff7 !important;
+ }
+
+ .react-tel-input .country:hover .country-name,
+ .react-tel-input .country:hover .dial-code {
+ color: #8f6ff7 !important;
+ }
+
+ .react-tel-input .country-list .highlight {
+ background-color: rgba(110, 65, 226, 0.15) !important;
+ color: #8f6ff7 !important;
+ }
+
+ .react-tel-input .country-list .highlight .country-name,
+ .react-tel-input .country-list .highlight .dial-code {
+ color: #8f6ff7 !important;
+ }
+ `;
+ document.head.appendChild(style);
+ };
+
+ applyHoverStyles();
+ }, []);
+
+
+ const getBorderColor = () => {
+ if (error) return theme.palette.error.main;
+ if (isFocused) return theme.palette.primary.main;
+
+return theme.palette.divider;
+ };
+
+ const handleFocus = (e: React.FocusEvent) => {
+ setIsFocused(true);
+ if (onFocus) onFocus(e);
+ };
+
+ const handleBlur = (e: React.FocusEvent) => {
+ setIsFocused(false);
+ if (onBlur) onBlur(e);
+ };
+
+ const handleChange = (phoneValue: string, data: any) => {
+ if (onChange) {
+ onChange(phoneValue, data);
+ }
+ };
+
+ return (
+
+ {label && (
+
+ {label}{required && ' '}
+
+ )}
+
+ {helperText && (
+
+ {helperText}
+
+ )}
+
+ );
+};
+
+export default ThemedPhoneInput;
+
+
diff --git a/src/views/RegisterUser.tsx b/src/views/RegisterUser.tsx
index d7901fc..922a982 100644
--- a/src/views/RegisterUser.tsx
+++ b/src/views/RegisterUser.tsx
@@ -10,6 +10,12 @@ import Stack from '@mui/material/Stack'
import type { SelectChangeEvent } from '@mui/material/Select'
+import type {CountryCode} from 'libphonenumber-js';
+
+import { parsePhoneNumberFromString} from 'libphonenumber-js'
+
+import 'react-phone-input-2/lib/style.css'
+
import {
Grid,
Box,
@@ -43,6 +49,8 @@ import { ResearcherRegister } from './RegisterResearcher'
import { PrivacyPolicyTerms } from './PrivacyPolicyTerms'
+import ThemedPhoneInput from '@/components/PhoneInput'
+
import { checkUserDuplicates } from '@/actions/register/registerActions'
// Define form interfaces
@@ -91,7 +99,7 @@ export const Register = () => {
const [accountType, setAccountType] = useState('patient')
const [success, setSuccess] = useState(null)
const [completedSteps, setCompletedSteps] = useState([])
- const [isPhoneFocused, setIsPhoneFocused] = useState(false)
+ const [, setIsPhoneFocused] = useState(false)
const [openPrivacyTerms, setOpenPrivacyTerms] = useState(false)
const [isFirstNameFocused, setIsFirstNameFocused] = useState(false)
const [isLastNameFocused, setIsLastNameFocused] = useState(false)
@@ -105,10 +113,6 @@ export const Register = () => {
const [isProfessionFocused, setIsProfessionFocused] = useState(false)
const [touchedFields, setTouchedFields] = useState>({})
- const validatePhoneNumber = (phoneNumber: string): boolean => {
- return /^\d{10,}$/.test(phoneNumber)
- }
-
const validateHospitalNumber = (hospitalNumber: string): boolean => {
const regex = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]{7}$/;
@@ -177,13 +181,31 @@ return regex.test(hospitalNumber);
const router = useRouter()
+ const formatToE164 = (phone: string, countryCode?: string) => {
+ try {
+ const parsed = parsePhoneNumberFromString(
+ phone,
+ (countryCode?.toUpperCase() as CountryCode) || 'GB'
+ )
+
+
+return parsed?.number || phone.trim().replace(/\s/g, '')
+ } catch (err) {
+ return phone.trim().replace(/\s/g, '')
+ }
+ }
+
// Define debouncedCheckDuplicates
- const debouncedCheckDuplicates = useCallback((email: string, phoneNumber: string, registrationNumber: string,hospitalNumber: string) => {
+ const debouncedCheckDuplicates = useCallback((email: string, phoneNumber: string, registrationNumber: string,hospitalNumber: string,countryCode?: string) => {
const checkDuplicates = debounce(async () => {
if (email.trim() || phoneNumber.trim() || registrationNumber.trim() || hospitalNumber.trim()) {
try {
- const result: DuplicateCheckResult = await checkUserDuplicates(email, phoneNumber, registrationNumber,hospitalNumber)
+ const formattedPhone = formatToE164(phoneNumber, countryCode)
+
+ const result: DuplicateCheckResult = await checkUserDuplicates(
+ email, formattedPhone, registrationNumber,hospitalNumber)
+ console.log('[checkUserDuplicates result]', result)
setFormErrors(prev => {
const newErrors = { ...prev }
@@ -290,16 +312,12 @@ return regex.test(hospitalNumber);
}
if (shouldValidateField('phoneNumber')) {
- if (formData.phoneNumber) {
- if (!validatePhoneNumber(formData.phoneNumber)) {
- errors.phoneNumber = 'Please enter a valid phone number'
- } else if (!errors.phoneNumber?.includes('already exists')) {
- errors.phoneNumber = ''
- }
- } else {
+ if (!formData.phoneNumber?.trim()) {
+ errors.phoneNumber = 'Phone number is required'
+ } else if (!errors.phoneNumber?.includes('already exists')) {
errors.phoneNumber = ''
}
-
+
isValid = isValid && !errors.phoneNumber
}
@@ -440,7 +458,9 @@ return regex.test(hospitalNumber);
}
if (fieldName === 'phoneNumber') {
- if (value && !validatePhoneNumber(value)) {
+ const digitsOnly = value.replace(/\D/g, '')
+
+ if (value && digitsOnly.length < 10) {
console.log('Phone validation failed')
newErrors.phoneNumber = 'Please enter a valid phone number'
} else {
@@ -495,28 +515,28 @@ return regex.test(hospitalNumber);
}
} else if (name === 'phoneNumber') {
console.log('Processing phone number input')
-
- if (value && !validatePhoneNumber(value)) {
- console.log('Phone validation failed in handleInputChange')
- setFormErrors(prev => ({ ...prev, phoneNumber: 'Please enter a valid phone number' }))
+
+ const fullValue = value.startsWith('+') ? value : `+${value}`
+
+ const parsed = parsePhoneNumberFromString(fullValue)
+
+ if (parsed?.isValid()) {
+
+ console.log('Phone number is valid ✅')
+ setFormErrors(prev => ({ ...prev, phoneNumber: '' }))
+
+ debouncedCheckDuplicates(
+ formData.email || '',
+ value,
+ accountType === 'clinician' ? formData.registrationNumber || '' : '',
+ accountType === 'patient' ? formData.hospitalNumber || '' : ''
+ )
} else {
- console.log('Phone validation passed in handleInputChange')
- setFormErrors(prev => {
- if (prev.phoneNumber && !prev.phoneNumber.includes('already exists')) {
- return { ...prev, phoneNumber: '' }
- }
-
- return prev
- })
-
- if (value) {
- debouncedCheckDuplicates(
- formData.email || '',
- value,
- accountType === 'clinician' ? formData.registrationNumber || '' : '',
- accountType === 'patient' ? formData.hospitalNumber || '' : ''
- )
- }
+ console.log('Invalid phone number ❌')
+ setFormErrors(prev => ({
+ ...prev,
+ phoneNumber: 'Please enter a valid phone number for the selected country'
+ }))
}
} else if (name === 'hospitalNumber') {
@@ -777,27 +797,33 @@ return regex.test(hospitalNumber);
/>
- {' '}
- ) => {
- setTouchedFields(prev => ({ ...prev, phoneNumber: true }))
- setFormData(prev => ({ ...prev, phoneNumber: e.target.value }))
- validateForm('phoneNumber')
- setIsFirstNameFocused(!!e.target.value)
+ {
+ setFormData(prev => ({ ...prev, phoneNumber: value }));
+ setTouchedFields(prev => ({ ...prev, phoneNumber: true }));
+
+ if (value) {
+ debouncedCheckDuplicates(
+ formData.email || '',
+ value,
+ accountType === 'clinician' ? formData.registrationNumber || '' : '',
+ accountType === 'patient' ? formData.hospitalNumber || '' : ''
+ );
+ }
+ }}
+ onFocus={() => setIsPhoneFocused(true)}
+ onBlur={() => {
+ setIsPhoneFocused(!!formData.phoneNumber);
+ setTouchedFields(prev => ({ ...prev, phoneNumber: true }));
+ validateForm('phoneNumber');
}}
error={!!formErrors.phoneNumber}
helperText={formErrors.phoneNumber}
- InputLabelProps={{ shrink: isPhoneFocused || !!formData.phoneNumber }}
- onFocus={() => setIsPhoneFocused(true)}
- sx={{ mb: 2, '& .MuiOutlinedInput-root': { borderRadius: '8px' } }}
- />{' '}
+ required={true}
+ country="gb"
+ />
)
}
+
+