diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 74b8230598c3d4caec3763eb080a8aca02f4cf59..e4924294bb2daafc499aade9225bf197b1c006c6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -29,6 +29,7 @@ "date-fns": "^2.24.0", "fetch-intercept": "^2.4.0", "http-proxy-middleware": "^2.0.1", + "i18n-iso-countries": "^6.8.0", "i18next": "^20.6.0", "i18next-browser-languagedetector": "^6.1.2", "i18next-http-backend": "^1.3.1", @@ -8460,6 +8461,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E=" + }, "node_modules/diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -11485,6 +11491,17 @@ "node": ">=8.12.0" } }, + "node_modules/i18n-iso-countries": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-6.8.0.tgz", + "integrity": "sha512-jJs/+CA6+VUICFxqGcB0vFMERGfhfvyNk+8Vb9EagSZkl7kSpm/kT0VyhvzM/zixDWEV/+oN9L7v/GT9BwzoGg==", + "dependencies": { + "diacritics": "1.3.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/i18next": { "version": "20.6.1", "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", @@ -16002,9 +16019,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.9.37", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.37.tgz", - "integrity": "sha512-RnUR4XwiVhMLnT7uFSdnmLeprspquuDtaShAgKTA+g/ms9/S4hQU3/QpFdh3iXPHtxD52QscXLm2W2+QBmvYAg==" + "version": "1.9.38", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.38.tgz", + "integrity": "sha512-7CCl9NZPYtX4JNXdvV5RnrQqrXp7LsLOTpYSUfEJBiySEnC5hysdwouXAuWgPDB55D/fpKm4RjM2/tUUh8kuoA==" }, "node_modules/lines-and-columns": { "version": "1.1.6", @@ -31314,6 +31331,11 @@ } } }, + "diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E=" + }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -33610,6 +33632,14 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" }, + "i18n-iso-countries": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-6.8.0.tgz", + "integrity": "sha512-jJs/+CA6+VUICFxqGcB0vFMERGfhfvyNk+8Vb9EagSZkl7kSpm/kT0VyhvzM/zixDWEV/+oN9L7v/GT9BwzoGg==", + "requires": { + "diacritics": "1.3.0" + } + }, "i18next": { "version": "20.6.1", "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", @@ -36935,9 +36965,9 @@ } }, "libphonenumber-js": { - "version": "1.9.37", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.37.tgz", - "integrity": "sha512-RnUR4XwiVhMLnT7uFSdnmLeprspquuDtaShAgKTA+g/ms9/S4hQU3/QpFdh3iXPHtxD52QscXLm2W2+QBmvYAg==" + "version": "1.9.38", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.9.38.tgz", + "integrity": "sha512-7CCl9NZPYtX4JNXdvV5RnrQqrXp7LsLOTpYSUfEJBiySEnC5hysdwouXAuWgPDB55D/fpKm4RjM2/tUUh8kuoA==" }, "lines-and-columns": { "version": "1.1.6", diff --git a/frontend/package.json b/frontend/package.json index 8048554994014c225236c156172982cb61f3a8b6..206b6ac5a3f96ffe01f7d9aa4b1c1c4fba923630 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,13 +17,14 @@ "@types/jest": "^26.0.24", "@types/node": "^12.20.24", "@types/react": "^17.0.20", - "@types/react-dom": "^17.0.9", "@types/react-datepicker": "^4.1.7", + "@types/react-dom": "^17.0.9", "@types/react-helmet": "^6.1.2", "@types/react-router-dom": "^5.1.8", "date-fns": "^2.24.0", "fetch-intercept": "^2.4.0", "http-proxy-middleware": "^2.0.1", + "i18n-iso-countries": "^6.8.0", "i18next": "^20.6.0", "i18next-browser-languagedetector": "^6.1.2", "i18next-http-backend": "^1.3.1", diff --git a/frontend/src/routes/guest/register/registerPage.tsx b/frontend/src/routes/guest/register/registerPage.tsx index a4233299690e1d8315ec79ba9bfdb54aded17d66..3aea9e90da8226af331324147dbd2b7f30adc3bc 100644 --- a/frontend/src/routes/guest/register/registerPage.tsx +++ b/frontend/src/routes/guest/register/registerPage.tsx @@ -1,12 +1,27 @@ -import { Box, Stack, TextField, Typography } from '@mui/material' +import { + Box, + MenuItem, + Select, + SelectChangeEvent, + Stack, + TextField, + Typography, +} from '@mui/material' import { SubmitHandler, useForm } from 'react-hook-form' -import React, { forwardRef, Ref, useImperativeHandle } from 'react' +import React, { forwardRef, Ref, useImperativeHandle, useState } from 'react' import { useTranslation } from 'react-i18next' +import { + CountryCallingCode, + getCountries, + getCountryCallingCode, +} from 'libphonenumber-js' import { ContactInformationBySponsor } from './guestDataForm' +import { getName } from 'i18n-iso-countries' import { EnteredGuestData } from './enteredGuestData' import { GuestRegisterCallableMethods } from './guestRegisterCallableMethods' import { isValidFnr, isValidMobilePhoneNumber } from '../../../utils' import AuthenticationMethod from './authenticationMethod' +import { CountryCallingCodes } from 'libphonenumber-js/types' interface GuestRegisterProperties { nextHandler(guestData: EnteredGuestData): void @@ -24,6 +39,23 @@ const GuestRegisterStep = forwardRef( const { i18n, t } = useTranslation(['common']) const { nextHandler, guestData } = props + const [countryCode, setCountryCode] = useState< + CountryCallingCode | undefined + >(undefined) + + const handleCountryCodeChange = (event: SelectChangeEvent) => { + if (event.target.value) { + const countryCodeType = getCountries().find((value) => { + return value.toString() === event.target.value + }) + if (countryCodeType) { + setCountryCode(getCountryCallingCode(countryCodeType)) + } + } else { + setCountryCode(undefined) + } + } + const submit: SubmitHandler<EnteredGuestData> = (data) => { nextHandler(data) } @@ -31,14 +63,21 @@ const GuestRegisterStep = forwardRef( const { register, handleSubmit, + setValue, + trigger, formState: { errors }, - } = useForm<EnteredGuestData>() - const onSubmit = handleSubmit(submit) + } = useForm() + const onSubmit = handleSubmit<EnteredGuestData>(submit) function doSubmit() { return onSubmit() } + register('mobilePhone', { + required: t<string>('validation.roleTypeRequired'), + validate: isValidMobilePhoneNumber, + }) + useImperativeHandle(ref, () => ({ doSubmit })) return ( @@ -78,15 +117,49 @@ const GuestRegisterStep = forwardRef( disabled /> - <TextField - id="mobilephone" - label={t('input.mobilePhone')} - error={!!errors.mobilePhone} - helperText={errors.mobilePhone && errors.mobilePhone.message} - {...register('mobilePhone', { - validate: isValidMobilePhoneNumber, - })} - /> + <Box + sx={{ + display: 'flex', + flexDirection: 'row', + paddingBottom: '2rem', + }} + > + <Select + sx={{ + maxHeight: '2.5rem', + minWidth: '5rem', + marginRight: '0.5rem', + }} + labelId="phone-number-select" + id="phone-number-select" + onChange={handleCountryCodeChange} + > + {getCountries().map((country) => ( + <MenuItem key={country} value={country}> + {getName(country, 'en')} +{getCountryCallingCode(country)} + </MenuItem> + ))} + </Select> + + <TextField + sx={{ flexGrow: 2 }} + label={t('input.mobilePhone')} + error={!!errors.mobilePhone} + helperText={errors.mobilePhone && errors.mobilePhone.message} + onChange={(value) => { + if (countryCode) { + // The country code and the rest of the mobile number are in two fields, so cannot + // register the field directly in form, but need to have extra logic defined + // to combine the values before writing them to the form handling + setValue( + 'mobilePhone', + `+${countryCode.toString()}${value.target.value}` + ) + trigger('mobilePhone') + } + }} + /> + </Box> {guestData.user_information_source === AuthenticationMethod.NationalIdNumberOrPassport && ( @@ -103,6 +176,10 @@ const GuestRegisterStep = forwardRef( <TextField id="national_id_number" label={t('input.nationalIdNumber')} + error={!!errors.national_id_number} + helperText={ + errors.nationalIdNumber && errors.nationalIdNumber.message + } {...register('nationalIdNumber', { validate: isValidFnr, })} @@ -126,7 +203,6 @@ const GuestRegisterStep = forwardRef( <Typography sx={{ paddingBottom: '1rem' }}> {t('guestRegisterWizardText.guestPeriodDescription')} </Typography> - <TextField id="ou-unit" value={ diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index d51a653a87117354ae68203e6f6cdda25befae54..40b95593383c15ecec08a67feddc1c8e1d83bc7e 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -16,6 +16,10 @@ import Footer from 'routes/components/footer' import Header from 'routes/components/header' import NotFound from 'routes/components/notFound' import ProtectedRoute from 'components/protectedRoute' +import { registerLocale } from 'i18n-iso-countries' +import i18n_iso_countries_en from 'i18n-iso-countries/langs/en.json' +import i18n_iso_countries_nb from 'i18n-iso-countries/langs/nb.json' +import i18n_iso_countries_nn from 'i18n-iso-countries/langs/nn.json' import GuestRegister from './guest/register' const AppWrapper = styled('div')({ @@ -29,6 +33,11 @@ const AppWrapper = styled('div')({ export default function App() { const { user, clearUserInfo } = useUserContext() + // Load country names for the supported languages + registerLocale(i18n_iso_countries_en) + registerLocale(i18n_iso_countries_nb) + registerLocale(i18n_iso_countries_nn) + // Intercept fetch responses fetchIntercept.register({ response(response) {