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 (
- Spider Logo + + Spider Logo +
) } 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" + /> ) } + +