From 95eaeffa85adac6fb9df1693610ee0310bc39179 Mon Sep 17 00:00:00 2001 From: Guts088737 Date: Wed, 9 Apr 2025 15:05:20 +0100 Subject: [PATCH] add international phone number; fix phone number verification; fix logo in login page(back to home) --- src/components/PhoneInput.tsx | 205 +++++++++++++++ .../layout/horizontal/NavbarContent.tsx | 238 +++++++++--------- src/views/Login.tsx | 7 +- src/views/RegisterUser.tsx | 139 ++++++---- 4 files changed, 417 insertions(+), 172 deletions(-) create mode 100644 src/components/PhoneInput.tsx diff --git a/src/components/PhoneInput.tsx b/src/components/PhoneInput.tsx new file mode 100644 index 0000000..dfa2cf9 --- /dev/null +++ b/src/components/PhoneInput.tsx @@ -0,0 +1,205 @@ +'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'); + const hoverColor = 'rgba(110, 65, 226, 0.15)'; + const hoverTextColor = '#8f6ff7'; + const inputBg = theme.palette.background.paper; + const divider = theme.palette.divider; + + style.innerHTML = ` + .react-tel-input .country:hover { + background-color: ${hoverColor} !important; + color: ${hoverTextColor} !important; + } + + .react-tel-input .country:hover .country-name, + .react-tel-input .country:hover .dial-code { + color: ${hoverTextColor} !important; + } + + .react-tel-input .country-list .highlight { + background-color: ${hoverColor} !important; + color: ${hoverTextColor} !important; + } + + .react-tel-input .country-list .highlight .country-name, + .react-tel-input .country-list .highlight .dial-code { + color: ${hoverTextColor} !important; + } + + .react-tel-input .flag-dropdown { + background-color: ${inputBg} !important; + border-top-left-radius: 8px; + border-bottom-left-radius: 8px; + border-right: 1px solid ${divider}; + } + + .react-tel-input .selected-flag { + background-color: transparent !important; + } + `; + + document.head.appendChild(style); + }; + + applyHoverStyles(); + }, [theme]); + + + 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/components/layout/horizontal/NavbarContent.tsx b/src/components/layout/horizontal/NavbarContent.tsx index 58db607..57b08c9 100644 --- a/src/components/layout/horizontal/NavbarContent.tsx +++ b/src/components/layout/horizontal/NavbarContent.tsx @@ -1,161 +1,173 @@ -'use client' +"use client" // Third-party Imports -import { useEffect, useState } from 'react' +import { Suspense } from 'react' import Image from 'next/image' +import Link from 'next/link' -import { usePathname } from 'next/navigation' +import dynamic from 'next/dynamic' import classnames from 'classnames' +// Component Imports import MenuItem from '@mui/material/MenuItem' -import { signOut, useSession } from 'next-auth/react' - -import { AccountStatus, Role } from '@prisma/client' - -import Link from '@/components/Link' +import { useSession, signOut } from 'next-auth/react' -// Component Imports import NavToggle from './NavToggle' + import ModeDropdown from '@components/layout/shared/ModeDropdown' + +// NextAuth Imports + // Hook Imports import useHorizontalNav from '@menu/hooks/useHorizontalNav' // Util Imports import { horizontalLayoutClasses } from '@layouts/utils/layoutClasses' -import { getPatientAgreedToResearch } from '@/actions/patient/consentActions' -import { useSettings } from '@/@core/hooks/useSettings' -const NavbarContent = () => { + +const NavbarContentInner = () => { // Hooks - const { data: session } = useSession() const { isBreakpointReached } = useHorizontalNav() - const [agreedToResearch, setAgreedToResearch] = useState(false) - const pathname = usePathname() - const { settings } = useSettings() + // Use NextAuth session + const { data: session, status } = useSession() + const loading = status === 'loading' + const user = session?.user - const userRole = session?.user?.role || 'GUEST' - const userId = session?.user?.id - const userStatus = session?.user.status + // Logout + const handleLogout = async () => { + console.log("Logout button clicked") - useEffect(() => { - const fetchResearchConsent = async () => { - if (userRole === Role.PATIENT && userId) { - const consent = await getPatientAgreedToResearch(userId) - - setAgreedToResearch(consent) - } + try { + await signOut({ redirect: false }) + window.location.href = '/home' + } catch (error) { + console.error('Failed to logout:', error) } + } - fetchResearchConsent() - }, [userRole, userId]) + // loading + if (loading) { + return ( +
+
Loading...
+
+ ) + } - const getMenuItems = () => { - if (userStatus === AccountStatus.PENDING || userStatus === AccountStatus.INACTIVE) { - return [{ label: 'Home', href: '/home' }] + //Generate menu items based on user roles + const renderRoleSpecificMenuItems = () => { + if (!user) { + // unregistered users + return ( + <> + The Spider + Log In + + ) } - if (userRole === 'GUEST') { - return [ - { label: 'Home', href: '/home' }, - { label: 'The Spider', href: '/questionnaire' } - ] - } else if (userRole === Role.RESEARCHER) { - return [ - { label: 'Home', href: '/home' }, - { label: 'Download', href: '/download' }, - { label: 'My Profile', href: '/my-profile' } - ] - } else if (userRole === Role.CLINICIAN) { - return [ - { label: 'Home', href: '/home' }, - { label: 'All Patients', href: '/all-patients' }, - { label: 'My Profile', href: '/my-profile' } - ] - } else if (userRole === Role.PATIENT) { - const patientItems = [ - { label: 'Home', href: '/home' }, - { label: 'The Spider', href: '/my-questionnaire' }, - { label: 'My Records', href: '/my-records' }, - { label: 'My Profile', href: '/my-profile' } - ] - - if (agreedToResearch) { - patientItems.splice(3, 0, { label: 'Studies', href: '/studies' }) - } - - return patientItems - } else if (userRole === Role.ADMIN) { - return [ - { label: 'Home', href: '/home' }, - { label: 'All Users', href: '/all-users' }, - { label: 'Studies', href: '/studies' }, - { label: 'My Profile', href: '/my-profile' } - ] + const role = user.role?.toLowerCase() || '' + + // Returns specific menu items based on role + switch (role) { + case 'clinician': + return ( + <> + All Patients + My Profile + Log Out + + ) + case "researcher": + return ( + <> + Download Data + My Profile + Log Out + + ) + case 'admin': + return ( + <> + All Users + My Profile + Log Out + + ) + case 'patient': + return ( + <> + The Spider + My Records + My Profile + Log Out + + ) + default: + return null } - - return [] } - const menuItems = getMenuItems() - return (
+ {/* Hide Logo on Smaller screens */} {!isBreakpointReached && ( - Spider Logo + + + + Spider Logo + + )}
- {menuItems.map(item => ( - - {item.label} - - ))} - {session ? ( - signOut({ callbackUrl: '/home', redirect: true })} - className={classnames('font-small plb-3 pli-1.5 mr-2 hover:text-primary')} - style={{ cursor: 'pointer' }} - > - Log Out - - ) : ( - - Log In - - )} - -
+ {!user && ( + + Home + + )} + + {/* Rendering role-based menu items */} + {renderRoleSpecificMenuItems()} + + +
) } +// dynamic loading,dont use SSR +const DynamicNavbarContent = dynamic(() => Promise.resolve(NavbarContentInner), { + ssr: false +}) + +const NavbarContent = () => { + return ( + +
Loading navbar...
+ + }> + +
+ ) +} + export default NavbarContent diff --git a/src/views/Login.tsx b/src/views/Login.tsx index f4ea81c..8684a82 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -102,10 +102,9 @@ const LoginV2 = ({ mode }: { mode: string }) => { return (
- - {' '} - - + + +
{`Log in`} {error && ( diff --git a/src/views/RegisterUser.tsx b/src/views/RegisterUser.tsx index d7901fc..3b0f6b3 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" + /> ) } + + +