From 360fac152e92a1feb362baa030f5c55b9e0b77c7 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 20 Jun 2024 12:16:38 -0400 Subject: [PATCH] Removing duplicate code. Added a function to validate date and if invalid, format the date. I've written tests for the functions --- .../PersonBirthday/PersonBirthday.tsx | 56 ++++++++++----- .../PersonShowMore/PersonShowMore.tsx | 69 ++++++++++++------- .../Items/PersonModal/personModalHelper.tsx | 5 ++ src/lib/intlFormat.test.ts | 33 ++++++++- src/lib/intlFormat.ts | 53 +++++++++++--- 5 files changed, 161 insertions(+), 55 deletions(-) diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonBirthday/PersonBirthday.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonBirthday/PersonBirthday.tsx index 302b51262..d40479fa0 100644 --- a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonBirthday/PersonBirthday.tsx +++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonBirthday/PersonBirthday.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import CakeIcon from '@mui/icons-material/Cake'; import { FormikProps } from 'formik'; import { DateTime } from 'luxon'; @@ -8,9 +8,12 @@ import { PersonCreateInput, PersonUpdateInput, } from 'src/graphql/types.generated'; +import { useLocale } from 'src/hooks/useLocale'; +import { validateAndFormatInvalidDate } from 'src/lib/intlFormat'; import { ModalSectionContainer } from '../ModalSectionContainer/ModalSectionContainer'; import { ModalSectionIcon } from '../ModalSectionIcon/ModalSectionIcon'; import { NewSocial } from '../PersonModal'; +import { buildDate } from '../personModalHelper'; interface PersonBirthdayProps { formikProps: FormikProps<(PersonUpdateInput | PersonCreateInput) & NewSocial>; @@ -20,39 +23,54 @@ export const PersonBirthday: React.FC = ({ formikProps, }) => { const { t } = useTranslation(); + const locale = useLocale(); + const [birthdayDateIsInvalid, setBirthdayDateIsInvalid] = useState(false); + const [backupBirthdayDate, setBackupBirthdayDate] = + useState | null>(null); const { values: { birthdayDay, birthdayMonth, birthdayYear }, setFieldValue, } = formikProps; - const handleDateChange = (date: DateTime | null) => { - setFieldValue('birthdayDay', date?.day || null); - setFieldValue('birthdayMonth', date?.month || null); - setFieldValue('birthdayYear', date?.year || null); - }; + useEffect(() => { + if (typeof birthdayMonth !== 'number' || typeof birthdayDay !== 'number') { + return; + } + + const date = validateAndFormatInvalidDate( + birthdayYear, + birthdayMonth, + birthdayDay, + locale, + ); - const birthdayDate = - birthdayMonth && birthdayDay - ? DateTime.local(birthdayYear ?? 1900, birthdayMonth, birthdayDay) - : null; + setBackupBirthdayDate( + date.formattedInvalidDate as unknown as DateTime, + ); + if (date.dateTime.invalidExplanation) { + setBirthdayDateIsInvalid(true); + } + }, [birthdayMonth, birthdayDay, birthdayYear]); - const invalidDate = - birthdayMonth === 0 && birthdayDay === 0 - ? DateTime.local(birthdayYear ?? 1900, birthdayMonth, birthdayDay) - .invalidExplanation !== '' - : false; + const handleDateChange = (date: DateTime | null) => { + setFieldValue('birthdayDay', date?.day ?? null); + setFieldValue('birthdayMonth', date?.month ?? null); + setFieldValue('birthdayYear', date?.year ?? null); + }; - const backupBirthdayDate = - `${birthdayMonth}/${birthdayDay}/${birthdayYear}` as unknown as DateTime; + const birthdayDate = useMemo( + () => buildDate(birthdayMonth, birthdayDay, birthdayYear), + [birthdayMonth, birthdayDay, birthdayYear], + ); return ( } /> handleDateChange(date)} /> diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonShowMore/PersonShowMore.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonShowMore/PersonShowMore.tsx index 8ffe06c21..e1ed438e4 100644 --- a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonShowMore/PersonShowMore.tsx +++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/PersonShowMore/PersonShowMore.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import BusinessIcon from '@mui/icons-material/Business'; import SchoolIcon from '@mui/icons-material/School'; import { @@ -20,11 +20,14 @@ import { PersonCreateInput, PersonUpdateInput, } from 'src/graphql/types.generated'; +import { useLocale } from 'src/hooks/useLocale'; +import { validateAndFormatInvalidDate } from 'src/lib/intlFormat'; import { RingIcon } from '../../../../../../RingIcon'; import { ModalSectionContainer } from '../ModalSectionContainer/ModalSectionContainer'; import { ModalSectionIcon } from '../ModalSectionIcon/ModalSectionIcon'; import { NewSocial } from '../PersonModal'; import { PersonSocial } from '../PersonSocials/PersonSocials'; +import { buildDate } from '../personModalHelper'; const DeceasedLabel = styled(FormControlLabel)(() => ({ margin: 'none', @@ -40,6 +43,11 @@ export const PersonShowMore: React.FC = ({ showDeceased = true, }) => { const { t } = useTranslation(); + const locale = useLocale(); + const [anniversaryDateIsInvalid, setAnniversaryDateIsInvalid] = + useState(false); + const [backupAnniversaryDate, setBackupAnniversaryDate] = + useState | null>(null); const { values: { @@ -58,32 +66,39 @@ export const PersonShowMore: React.FC = ({ setFieldValue, } = formikProps; - const handleDateChange = (date: DateTime | null) => { - setFieldValue('anniversaryDay', date?.day || null); - setFieldValue('anniversaryMonth', date?.month || null); - setFieldValue('anniversaryYear', date?.year || null); - }; + useEffect(() => { + if ( + typeof anniversaryMonth !== 'number' || + typeof anniversaryDay !== 'number' + ) { + return; + } - const anniversaryDate = - anniversaryMonth && anniversaryDay - ? DateTime.local( - anniversaryYear ?? 1900, - anniversaryMonth, - anniversaryDay, - ) - : null; + const date = validateAndFormatInvalidDate( + anniversaryYear, + anniversaryMonth, + anniversaryDay, + locale, + ); - const invalidAnniversaryDate = - anniversaryMonth === 0 && anniversaryDay === 0 - ? DateTime.local( - anniversaryYear ?? 1900, - anniversaryMonth, - anniversaryDay, - ).invalidExplanation !== '' - : false; + setBackupAnniversaryDate( + date.formattedInvalidDate as unknown as DateTime, + ); + if (date.dateTime.invalidExplanation) { + setAnniversaryDateIsInvalid(true); + } + }, [anniversaryMonth, anniversaryDay, anniversaryYear]); - const backupAnniversaryDate = - `${anniversaryMonth}/${anniversaryDay}/${anniversaryYear}` as unknown as DateTime; + const handleDateChange = (date: DateTime | null) => { + setFieldValue('anniversaryDay', date?.day ?? null); + setFieldValue('anniversaryMonth', date?.month ?? null); + setFieldValue('anniversaryYear', date?.year ?? null); + }; + + const anniversaryDate = useMemo( + () => buildDate(anniversaryMonth, anniversaryDay, anniversaryYear), + [anniversaryMonth, anniversaryDay, anniversaryYear], + ); return ( <> @@ -164,9 +179,11 @@ export const PersonShowMore: React.FC = ({ date && handleDateChange(date)} /> diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/personModalHelper.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/personModalHelper.tsx index ca9acbfb9..7f567b3c6 100644 --- a/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/personModalHelper.tsx +++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/People/Items/PersonModal/personModalHelper.tsx @@ -1,3 +1,4 @@ +import { DateTime } from 'luxon'; import { TFunction } from 'react-i18next'; import * as yup from 'yup'; import { @@ -263,3 +264,7 @@ export const formatSubmittedFields = ( ), }; }; + +export const buildDate = (month, day, year) => { + return month && day ? DateTime.local(year ?? 1900, month, day) : null; +}; diff --git a/src/lib/intlFormat.test.ts b/src/lib/intlFormat.test.ts index a94e4b0f1..f38bd8668 100644 --- a/src/lib/intlFormat.test.ts +++ b/src/lib/intlFormat.test.ts @@ -9,6 +9,7 @@ import { monthYearFormat, numberFormat, percentageFormat, + validateAndFormatInvalidDate, } from './intlFormat'; describe('intlFormat', () => { @@ -186,7 +187,19 @@ describe('intlFormat', () => { expect(date).toBeNull(); }); - it('returns if month is null', () => { + it('handle an invalid date', () => { + const date = dateFromParts(2000, 0, 0, locale); + + expect(date).toBe('0/0/2000 - Invalid Date, please fix.'); + }); + + it('handle an invalid date without a year', () => { + const date = dateFromParts(null, 0, 0, locale); + + expect(date).toBe('0/0/2020 - Invalid Date, please fix.'); + }); + + it('handle an invalid date where we can not format the invalid date', () => { const date = dateFromParts(0, 0, 2000, locale); expect(date).toBe( @@ -194,6 +207,24 @@ describe('intlFormat', () => { ); }); }); + + describe('validateAndFormatInvalidDate', () => { + const locale = 'en-US'; + it('returns invalid date en-US formatted', () => { + const date = validateAndFormatInvalidDate(2000, 0, 0, locale); + expect(date.formattedInvalidDate).toBe('0/0/2000'); + }); + + it('returns invalid date en-UK formatted', () => { + const date = validateAndFormatInvalidDate(2000, 0, 0, 'en-UK'); + expect(date.formattedInvalidDate).toBe('0/00/2000'); + }); + + it('returns invalid date de formatted', () => { + const date = validateAndFormatInvalidDate(2000, 0, 0, 'de'); + expect(date.formattedInvalidDate).toBe('0.0.2000'); + }); + }); //this test often fails locally. It passes on github. describe('dateTimeFormat', () => { const locale = 'en-US'; diff --git a/src/lib/intlFormat.ts b/src/lib/intlFormat.ts index 8b19e27b2..79632cfad 100644 --- a/src/lib/intlFormat.ts +++ b/src/lib/intlFormat.ts @@ -88,17 +88,17 @@ export const dateFromParts = ( return null; } - if (typeof year === 'number') { - const date = DateTime.local(year, month, day); - if (date.invalidReason || date.invalidExplanation) { - return `Invalid Date - ${date.invalidExplanation}`; + const date = validateAndFormatInvalidDate(year, month, day, locale); + if (date.dateTime.invalidExplanation) { + if (date.formattedInvalidDate) { + return `${date.formattedInvalidDate} - Invalid Date, please fix.`; + } else { + return `Invalid Date - ${date.dateTime.invalidExplanation}`; } - return dateFormat(date, locale); + } + if (typeof year === 'number') { + return dateFormat(date.dateTime, locale); } else { - const date = DateTime.local().set({ month, day }); - if (date.invalidReason || date.invalidExplanation) { - return `Invalid Date - ${date.invalidExplanation}`; - } return dayMonthFormat(day, month, locale); } }; @@ -120,6 +120,41 @@ export const dateTimeFormat = ( }).format(date.toJSDate()); }; +export const validateAndFormatInvalidDate = ( + year: number | null | undefined, + month: number, + day: number, + locale: string, +) => { + const yyyy = year ?? DateTime.local().year; + const date = DateTime.local(yyyy, month, day); + let formattedInvalidDate = ''; + if (date.invalidExplanation && month === 0 && day === 0) { + const placeholderYear = 2024; + const placeholderMonth = 8; + const placeholderDay = 15; + const placeholderDate = new Date( + placeholderYear, + placeholderMonth - 1, + placeholderDay, + ); + const formattedPlaceholderDate = dateFormatShort( + DateTime.fromISO(placeholderDate.toISOString()), + locale, + ); + + formattedInvalidDate = formattedPlaceholderDate + .replace(`${placeholderYear}`, `${yyyy}`) + .replace(`${placeholderMonth}`, `${month}`) + .replace(`${placeholderDay}`, `${day}`); + } + + return { + formattedInvalidDate, + dateTime: date, + }; +}; + const intlFormat = { numberFormat, percentageFormat,