diff --git a/src/components/Checkbox/Checkbox.tsx b/src/components/Checkbox/Checkbox.tsx index ae423bd97..e4ddbfd1a 100644 --- a/src/components/Checkbox/Checkbox.tsx +++ b/src/components/Checkbox/Checkbox.tsx @@ -33,7 +33,7 @@ const Checkbox: React.FC = ({ label, name, onChange, header, checked, val ) : null}
- +
{helperText ?
{helperText}
: null} diff --git a/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap b/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap index 3f632029a..65a4973f6 100644 --- a/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap +++ b/src/components/Checkbox/__snapshots__/Checkbox.test.tsx.snap @@ -17,6 +17,7 @@ exports[` renders and matches snapshot 1`] = ` diff --git a/src/components/EditPasswordForm/EditPasswordForm.module.scss b/src/components/EditPasswordForm/EditPasswordForm.module.scss index 265c663f9..f4c647fcf 100644 --- a/src/components/EditPasswordForm/EditPasswordForm.module.scss +++ b/src/components/EditPasswordForm/EditPasswordForm.module.scss @@ -15,6 +15,8 @@ .button { margin-bottom: 8px; } -.link { + +.link, +.textField { margin-bottom: 24px; } diff --git a/src/components/EditPasswordForm/EditPasswordForm.test.tsx b/src/components/EditPasswordForm/EditPasswordForm.test.tsx index fbc7a10b6..e58474859 100644 --- a/src/components/EditPasswordForm/EditPasswordForm.test.tsx +++ b/src/components/EditPasswordForm/EditPasswordForm.test.tsx @@ -6,7 +6,7 @@ import EditPasswordForm from './EditPasswordForm'; describe('', () => { test('renders and matches snapshot', () => { const { container } = render( - , + , ); expect(container).toMatchSnapshot(); diff --git a/src/components/EditPasswordForm/EditPasswordForm.tsx b/src/components/EditPasswordForm/EditPasswordForm.tsx index fc0f7394b..efdb43e93 100644 --- a/src/components/EditPasswordForm/EditPasswordForm.tsx +++ b/src/components/EditPasswordForm/EditPasswordForm.tsx @@ -18,13 +18,14 @@ import styles from './EditPasswordForm.module.scss'; type Props = { onSubmit: React.FormEventHandler; onChange: React.ChangeEventHandler; + onBlur: React.FocusEventHandler; error?: string; errors: FormErrors; value: EditPasswordFormData; submitting: boolean; }; -const EditPasswordForm: React.FC = ({ onSubmit, onChange, value, errors, submitting }: Props) => { +const EditPasswordForm: React.FC = ({ onSubmit, onChange, onBlur, value, errors, submitting }: Props) => { const { t } = useTranslation('account'); const [viewPassword, toggleViewPassword] = useToggle(); @@ -33,12 +34,19 @@ const EditPasswordForm: React.FC = ({ onSubmit, onChange, value, errors,

{t('reset.password_reset')}

{errors.form ? {errors.form} : null} + + {t('reset.password_helper_text')} + + )} name="password" type={viewPassword ? 'text' : 'password'} rightControl={ @@ -48,7 +56,6 @@ const EditPasswordForm: React.FC = ({ onSubmit, onChange, value, errors, } required /> - +

+ login.not_registered + + + login.sign_up + +

`; diff --git a/src/components/PasswordStrength/PasswordStrength.module.scss b/src/components/PasswordStrength/PasswordStrength.module.scss index 0deec91e9..4de7fa81d 100644 --- a/src/components/PasswordStrength/PasswordStrength.module.scss +++ b/src/components/PasswordStrength/PasswordStrength.module.scss @@ -1,9 +1,30 @@ @use '../../styles/variables'; @use '../../styles/theme'; +@mixin strength($strength, $width, $color) { + &[data-strength="#{$strength}"] { + .passwordStrengthFill { + width: $width; + background: $color; + } + + .label { + color: $color; + } + } +} + .passwordStrength { + @include strength(1, 25%, orangered); + @include strength(2, 50%, orange); + @include strength(3, 75%, yellowgreen); + @include strength(4, 100%, green); + position: relative; - margin: variables.$base-spacing 0; + display: flex; + align-items: center; + height: 16px; + margin: 8px 0; font-size: 14px; } @@ -11,7 +32,7 @@ position: relative; width: 170px; height: 6px; - margin: variables.$base-spacing 0; + margin-right: 8px; background: #ddd; border-radius: 5px; @@ -20,28 +41,13 @@ .passwordStrengthFill { position: absolute; width: 0; - height: inherit; + height: 100%; background: transparent; border-radius: inherit; transition: width 0.5s ease-in-out, background 0.25s; } -.passwordStrengthFill[data-strength='1'] { - width: 25%; - background: orangered; -} - -.passwordStrengthFill[data-strength='2'] { - width: 50%; - background: orange; -} - -.passwordStrengthFill[data-strength='3'] { - width: 75%; - background: yellowgreen; -} - -.passwordStrengthFill[data-strength='4'] { - width: 100%; - background: green; +.label { + font-weight: theme.$body-font-weight-bold; + font-size: 14px; } diff --git a/src/components/PasswordStrength/PasswordStrength.tsx b/src/components/PasswordStrength/PasswordStrength.tsx index e6b1411ef..0ec0237b3 100644 --- a/src/components/PasswordStrength/PasswordStrength.tsx +++ b/src/components/PasswordStrength/PasswordStrength.tsx @@ -7,32 +7,51 @@ type Props = { password: string; }; +const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[0-9]).{8,}$/; + const PasswordStrength: React.FC = ({ password }: Props) => { const { t } = useTranslation('account'); + const passwordStrength = (password: string) => { let strength = 0; - if (password.match(/[a-z]+/)) { + if (!password.match(PASSWORD_REGEX)) return strength; + + if (password.match(/[A-Z]+/)) { strength += 1; } - if (password.match(/[A-Z]+/)) { + + if (password.match(/(\d.*\d)/)) { strength += 1; } - if (password.match(/[0-9|!@#$%^&*()_+-=]+/)) { + + if (password.match(/[!,@#$%^&*?_~]/)) { strength += 1; } - if (password.length >= 6) { + + if (password.match(/([!,@#$%^&*?_~].*[!,@#$%^&*?_~])/)) { strength += 1; } return strength; }; + const strength = passwordStrength(password); + const labels = [ + t('registration.password_strength.invalid'), + t('registration.password_strength.weak'), + t('registration.password_strength.fair'), + t('registration.password_strength.strong'), + t('registration.password_strength.very_strong'), + ]; + + if (!strength) return null; + return ( -
+
-
-
- {t('registration.password_strength')} +
+
{' '} + {labels[strength]}
); }; diff --git a/src/components/PasswordStrength/__snapshots__/PasswordStrength.test.tsx.snap b/src/components/PasswordStrength/__snapshots__/PasswordStrength.test.tsx.snap index 4370f4a3b..2f312e3a7 100644 --- a/src/components/PasswordStrength/__snapshots__/PasswordStrength.test.tsx.snap +++ b/src/components/PasswordStrength/__snapshots__/PasswordStrength.test.tsx.snap @@ -4,17 +4,20 @@ exports[` renders and matches snapshot 1`] = `
- - registration.password_strength + + + registration.password_strength.fair
diff --git a/src/components/RegistrationForm/RegistrationForm.test.tsx b/src/components/RegistrationForm/RegistrationForm.test.tsx index 5cf844892..c0b99cd13 100644 --- a/src/components/RegistrationForm/RegistrationForm.test.tsx +++ b/src/components/RegistrationForm/RegistrationForm.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { render } from '@testing-library/react'; + +import { render } from '../../testUtils'; import RegistrationForm from './RegistrationForm'; @@ -9,12 +10,14 @@ describe('', () => { , ); diff --git a/src/components/RegistrationForm/RegistrationForm.tsx b/src/components/RegistrationForm/RegistrationForm.tsx index cc8a2c6b3..45019c9ce 100644 --- a/src/components/RegistrationForm/RegistrationForm.tsx +++ b/src/components/RegistrationForm/RegistrationForm.tsx @@ -15,12 +15,14 @@ import PasswordStrength from '../PasswordStrength/PasswordStrength'; import Checkbox from '../Checkbox/Checkbox'; import FormFeedback from '../FormFeedback/FormFeedback'; import LoadingOverlay from '../LoadingOverlay/LoadingOverlay'; +import Link from '../Link/Link'; import styles from './RegistrationForm.module.scss'; type Props = { onSubmit: React.FormEventHandler; onChange: React.ChangeEventHandler; + onBlur: React.FocusEventHandler; onConsentChange: React.ChangeEventHandler; errors: FormErrors; values: RegistrationFormData; @@ -28,16 +30,19 @@ type Props = { consentValues: Record; consentErrors: string[]; submitting: boolean; + canSubmit: boolean; publisherConsents?: Consent[]; }; const RegistrationForm: React.FC = ({ onSubmit, onChange, + onBlur, values, errors, submitting, loading, + canSubmit, publisherConsents, consentValues, onConsentChange, @@ -48,10 +53,6 @@ const RegistrationForm: React.FC = ({ const { t } = useTranslation('account'); const history = useHistory(); - const loginButtonClickHandler = () => { - history.push(addQueryParam(history, 'u', 'login')); - }; - const formatConsentLabel = (label: string): string | JSX.Element => { // @todo sanitize consent label to prevent XSS const hasHrefOpenTag = //.test(label); @@ -79,6 +80,7 @@ const RegistrationForm: React.FC = ({ = ({ + + {t('registration.password_helper_text')} + + )} name="password" type={viewPassword ? 'text' : 'password'} rightControl={ @@ -106,13 +114,13 @@ const RegistrationForm: React.FC = ({ } required /> - {publisherConsents?.map((consent, index) => ( = ({ variant="contained" color="primary" size="large" - disabled={submitting} + disabled={submitting || !canSubmit} fullWidth /> -
- {t('registration.already_account')} - -
+

+ {t('registration.already_account')} {t('login.sign_in')} +

{submitting && } ); diff --git a/src/components/RegistrationForm/__snapshots__/RegistrationForm.test.tsx.snap b/src/components/RegistrationForm/__snapshots__/RegistrationForm.test.tsx.snap index c8b35f4d8..420ea94e9 100644 --- a/src/components/RegistrationForm/__snapshots__/RegistrationForm.test.tsx.snap +++ b/src/components/RegistrationForm/__snapshots__/RegistrationForm.test.tsx.snap @@ -80,21 +80,11 @@ exports[` renders and matches snapshot 1`] = `
- -
-
+ registration.password_helper_text
- - registration.password_strength -
-
- - registration.already_account - - -
+ +

`; diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index d031abddf..991d11c8a 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -14,7 +14,8 @@ type Props = { value: string; type?: 'text' | 'email' | 'password' | 'search' | 'number' | 'date'; onChange?: React.ChangeEventHandler; - onFocus?: React.ChangeEventHandler; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; helperText?: React.ReactNode; leftControl?: React.ReactNode; rightControl?: React.ReactNode; diff --git a/src/containers/AccountModal/forms/EditPassword.tsx b/src/containers/AccountModal/forms/EditPassword.tsx index ae2c25b00..8df3d73bf 100644 --- a/src/containers/AccountModal/forms/EditPassword.tsx +++ b/src/containers/AccountModal/forms/EditPassword.tsx @@ -17,9 +17,13 @@ const ResetPassword: React.FC = () => { const emailParam = useQueryParam('email'); const passwordSubmitHandler: UseFormOnSubmitHandler = async (formData, { setErrors, setSubmitting, setValue }) => { - try { - if (!resetPasswordTokenParam || !emailParam) throw new Error('invalid reset link'); + if (!emailParam || !resetPasswordTokenParam) { + setErrors({ form: t('reset.invalid_link') }); + + return setSubmitting(false); + } + try { await changePassword(emailParam, formData.password, resetPasswordTokenParam); history.push(addQueryParam(history, 'u', 'login')); } catch (error: unknown) { @@ -28,8 +32,6 @@ const ResetPassword: React.FC = () => { setErrors({ password: t('reset.invalid_password') }); } else if (error.message.includes('resetPasswordToken is not valid')) { setErrors({ password: t('reset.invalid_token') }); - } else if (error.message.includes('invalid reset link')) { - setErrors({ password: t('reset.invalid_link') }); } setValue('password', ''); @@ -42,20 +44,22 @@ const ResetPassword: React.FC = () => { { password: '' }, passwordSubmitHandler, object().shape({ - password: string().required(t('login.field_required')), + password: string() + .matches(/^(?=.*[a-z])(?=.*[0-9]).{8,}$/, t('registration.invalid_password')) + .required(t('login.field_required')), }), + true, ); return ( - - - + ); }; diff --git a/src/containers/AccountModal/forms/Login.tsx b/src/containers/AccountModal/forms/Login.tsx index 20862e855..c559823e6 100644 --- a/src/containers/AccountModal/forms/Login.tsx +++ b/src/containers/AccountModal/forms/Login.tsx @@ -8,8 +8,10 @@ import LoginForm from '../../../components/LoginForm/LoginForm'; import { login } from '../../../stores/AccountStore'; import useForm, { UseFormOnSubmitHandler } from '../../../hooks/useForm'; import { removeQueryParam } from '../../../utils/history'; +import { ConfigStore } from '../../../stores/ConfigStore'; const Login = () => { + const { siteName } = ConfigStore.useState((s) => s.config); const history = useHistory(); const { t } = useTranslation('account'); const loginSubmitHandler: UseFormOnSubmitHandler = async (formData, { setErrors, setSubmitting, setValue }) => { @@ -39,7 +41,7 @@ const Login = () => { const initialValues: LoginFormData = { email: '', password: '' }; const { handleSubmit, handleChange, values, errors, submitting } = useForm(initialValues, loginSubmitHandler, validationSchema); - return ; + return ; }; export default Login; diff --git a/src/containers/AccountModal/forms/Registration.tsx b/src/containers/AccountModal/forms/Registration.tsx index 592567e4f..36dc2cb7e 100644 --- a/src/containers/AccountModal/forms/Registration.tsx +++ b/src/containers/AccountModal/forms/Registration.tsx @@ -73,16 +73,17 @@ const Registration = () => { const validationSchema: SchemaOf = object().shape({ email: string().email(t('registration.field_is_not_valid_email')).required(t('registration.field_required')), - password: string().required(t('registration.field_required')), + password: string().matches(/^(?=.*[a-z])(?=.*[0-9]).{8,}$/, t('registration.invalid_password')).required(t('registration.field_required')), }); const initialRegistrationValues: RegistrationFormData = { email: '', password: '' }; - const { handleSubmit, handleChange, values, errors, submitting } = useForm(initialRegistrationValues, registrationSubmitHandler, validationSchema); + const { handleSubmit, handleChange, handleBlur, values, errors, submitting } = useForm(initialRegistrationValues, registrationSubmitHandler, validationSchema, true); return ( { publisherConsents={publisherConsents} loading={publisherConsentsLoading} onConsentChange={handleChangeConsent} + canSubmit={!!values.email && !!values.password} /> ); }; diff --git a/src/containers/AccountModal/forms/ResetPassword.tsx b/src/containers/AccountModal/forms/ResetPassword.tsx index 536dd3cfd..3246613ba 100644 --- a/src/containers/AccountModal/forms/ResetPassword.tsx +++ b/src/containers/AccountModal/forms/ResetPassword.tsx @@ -20,7 +20,7 @@ const ResetPassword: React.FC = ({ type }: Prop) => { const { t } = useTranslation('account'); const history = useHistory(); const user = AccountStore.useState((state) => state.user); - const [resetPasswordSubmtting, setResetPasswordSubmitting] = useState(false); + const [resetPasswordSubmitting, setResetPasswordSubmitting] = useState(false); const cancelClickHandler = () => { history.push(removeQueryParam(history, 'u')); @@ -31,7 +31,7 @@ const ResetPassword: React.FC = ({ type }: Prop) => { }; const resetPasswordClickHandler = async () => { - const resetUrl = `${window.location.origin}/u/my-account?u=edit-password`; + const resetUrl = `${window.location.origin}/?u=edit-password`; try { if (!user?.email) throw new Error('invalid param email'); @@ -51,7 +51,7 @@ const ResetPassword: React.FC = ({ type }: Prop) => { }; const emailSubmitHandler: UseFormOnSubmitHandler = async (formData, { setErrors, setSubmitting }) => { - const resetUrl = `${window.location.origin}/u/my-account?u=edit-password`; + const resetUrl = `${window.location.origin}/?u=edit-password`; try { await resetPassword(formData.email, resetUrl); @@ -72,12 +72,13 @@ const ResetPassword: React.FC = ({ type }: Prop) => { object().shape({ email: string().email(t('login.field_is_not_valid_email')).required(t('login.field_required')), }), + true, ); return ( {type === 'reset' && ( - + )} {type === 'forgot' && ( = ({ type }: Prop) => { /> )} {type === 'confirmation' && } - {(emailForm.submitting || resetPasswordSubmtting) && } + {(emailForm.submitting || resetPasswordSubmitting) && } ); }; diff --git a/src/containers/Cinema/Cinema.tsx b/src/containers/Cinema/Cinema.tsx index 8dc66de7f..59a7ce8a5 100644 --- a/src/containers/Cinema/Cinema.tsx +++ b/src/containers/Cinema/Cinema.tsx @@ -11,6 +11,7 @@ import { watchHistoryStore, useWatchHistory } from '../../stores/WatchHistorySto import { ConfigContext } from '../../providers/ConfigProvider'; import { addScript } from '../../utils/dom'; import useOttAnalytics from '../../hooks/useOttAnalytics'; +import { deepCopy } from '../../utils/collection'; import styles from './Cinema.module.scss'; @@ -64,15 +65,7 @@ const Cinema: React.FC = ({ item, onPlay, onPause, onComplete, onUserActi } // load new item - playerRef.current.load([ - { - mediaid: item.mediaid, - image: item.image, - title: item.title, - description: item.description, - sources: item.sources.map((source) => ({ ...source })), - }, - ]); + playerRef.current.load([deepCopy(item)]); }; const initializePlayer = () => { @@ -85,7 +78,7 @@ const Cinema: React.FC = ({ item, onPlay, onPause, onComplete, onUserActi playerRef.current = window.jwplayer(playerElementRef.current); playerRef.current.setup({ - playlist: [item], + playlist: [deepCopy(item)], aspect: false, width: '100%', height: '100%', diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index 1713912cd..46cefb862 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -1,5 +1,5 @@ import { useState } from 'react'; -import type { FormErrors, GenericFormValues, UseFormChangeHandler, UseFormSubmitHandler } from 'types/form'; +import type { FormErrors, GenericFormValues, UseFormChangeHandler, UseFormBlurHandler, UseFormSubmitHandler } from 'types/form'; import { ValidationError, AnySchema } from 'yup'; export type UseFormReturnValue = { @@ -7,6 +7,7 @@ export type UseFormReturnValue = { errors: FormErrors; submitting: boolean; handleChange: UseFormChangeHandler; + handleBlur: UseFormBlurHandler; handleSubmit: UseFormSubmitHandler; setValue: (key: keyof T, value: string) => void; setErrors: (errors: FormErrors) => void; @@ -26,19 +27,53 @@ export default function useForm( initialValues: T, onSubmit: UseFormOnSubmitHandler, validationSchema?: AnySchema, + validateOnBlur: boolean = false, ): UseFormReturnValue { + const [touched, setTouched] = useState>( + Object.fromEntries((Object.keys(initialValues) as Array).map((key) => [key, false])) as Record, + ); const [values, setValues] = useState(initialValues); const [submitting, setSubmitting] = useState(false); const [errors, setErrors] = useState>({}); + const validateField = (name: string, formValues: T) => { + if (!validationSchema) return; + + try { + validationSchema.validateSyncAt(name, formValues); + + // clear error + setErrors((errors) => ({ ...errors, [name]: null })); + } catch (error: unknown) { + if (error instanceof ValidationError) { + const errorMessage = error.errors[0]; + setErrors((errors) => ({ ...errors, [name]: errorMessage })); + } + } + }; + const setValue = (name: keyof T, value: string | boolean) => { setValues((current) => ({ ...current, [name]: value })); }; const handleChange: UseFormChangeHandler = (event) => { + const name = event.target.name; const value = event.target instanceof HTMLInputElement && event.target.type === 'checkbox' ? event.target.checked : event.target.value; - setValues((current) => ({ ...current, [event.target.name]: value })); + const newValues = { ...values, [name]: value }; + + setValues(newValues); + setTouched(current => ({ ...current, [name]: value })); + + if (errors[name]) { + validateField(name, newValues) + } + }; + + const handleBlur: UseFormBlurHandler = (event) => { + if (!validateOnBlur || !touched[event.target.name]) return; + + validateField(event.target.name, values); }; const validate = (validationSchema: AnySchema) => { @@ -85,5 +120,5 @@ export default function useForm( onSubmit(values, { setValue, setErrors, setSubmitting, validate }); }; - return { values, errors, handleChange, handleSubmit, submitting, setValue, setErrors, setSubmitting }; + return { values, errors, handleChange, handleBlur, handleSubmit, submitting, setValue, setErrors, setSubmitting }; } diff --git a/src/i18n/locales/en_US/account.json b/src/i18n/locales/en_US/account.json index d50f67074..05fa538af 100644 --- a/src/i18n/locales/en_US/account.json +++ b/src/i18n/locales/en_US/account.json @@ -57,8 +57,10 @@ "field_required": "This field is required", "forgot_password": "Forgot password?", "hide_password": "Hide password", + "not_registered": "New to {{siteName}}?", "password": "Password", "sign_in": "Sign in", + "sign_up": "Sign up", "view_password": "View password", "wrong_combination": "Incorrect email/password combination", "wrong_email": "Please check your email and try again." @@ -97,9 +99,16 @@ "field_is_not_valid_email": "Please re-enter your email details", "field_required": "This field is required", "hide_password": "Hide password", - "invalid_password": "Use a minimum of 6 characters (case sensitive) with at least one number or special character and one capital character", + "invalid_password": "Use a minimum of 8 characters (case sensitive) with at least one number", "password": "Password", - "password_strength": "Use a minimum of 6 characters (case sensitive) with at least one number or special character and one capital character", + "password_helper_text": "Use a minimum of 8 characters (case sensitive) with at least one number", + "password_strength": { + "fair": "Fair", + "invalid": "", + "strong": "Strong", + "very_strong": "Very strong", + "weak": "Weak" + }, "sign_up": "Sign up", "user_exists": "There is already a user with this email address", "view_password": "View password", @@ -126,9 +135,11 @@ "invalid_token": "Invalid link", "link_sent": "Password link sent", "link_sent_text": "Please check your inbox at {{email}}", + "new_password": "New password", "no": "No, thanks", "not_sure": "Not sure that was the right email address?", "password": "Password", + "password_helper_text": "Use a minimum of 8 characters (case sensitive) with at least one number", "password_reset": "Password reset", "reset_password": "Edit Password", "text": "If you want to edit your password, click 'YES, Reset' to receive password reset instruction on your mail", diff --git a/src/i18n/locales/nl_NL/account.json b/src/i18n/locales/nl_NL/account.json index 84f23c26b..2bf628e80 100644 --- a/src/i18n/locales/nl_NL/account.json +++ b/src/i18n/locales/nl_NL/account.json @@ -57,8 +57,10 @@ "field_required": "", "forgot_password": "", "hide_password": "", + "not_registered": "", "password": "", "sign_in": "", + "sign_up": "", "view_password": "", "wrong_combination": "", "wrong_email": "" @@ -95,7 +97,14 @@ "hide_password": "", "invalid_password": "", "password": "", - "password_strength": "", + "password_helper_text": "", + "password_strength": { + "fair": "", + "invalid": "", + "strong": "", + "very_strong": "", + "weak": "" + }, "sign_up": "", "user_exists": "", "view_password": "", @@ -122,9 +131,11 @@ "invalid_token": "", "link_sent": "", "link_sent_text": "", + "new_password": "", "no": "", "not_sure": "", "password": "", + "password_helper_text": "", "password_reset": "", "reset_password": "", "text": "", diff --git a/src/utils/collection.ts b/src/utils/collection.ts index 77eb13ba9..a2410d628 100644 --- a/src/utils/collection.ts +++ b/src/utils/collection.ts @@ -126,6 +126,13 @@ const checkConsentsFromValues = (publisherConsents: Consent[], consents: Record< return { customerConsents, consentsErrors }; }; +const deepCopy = (obj: unknown) => { + if (Array.isArray(obj) || (typeof obj === 'object' && obj !== null)) { + return JSON.parse(JSON.stringify(obj)); + } + return obj; +}; + export { getFiltersFromConfig, getFiltersFromSeries, @@ -138,4 +145,5 @@ export { formatConsentsFromValues, extractConsentValues, checkConsentsFromValues, + deepCopy, }; diff --git a/types/form.d.ts b/types/form.d.ts index 766b22176..b93f11f13 100644 --- a/types/form.d.ts +++ b/types/form.d.ts @@ -1,4 +1,5 @@ export type UseFormChangeHandler = React.ChangeEventHandler; +export type UseFormBlurHandler = React.FocusEventHandler; export type UseFormSubmitHandler = React.FormEventHandler; export type GenericFormErrors = { form: string };