From 3af0524bc9100241b44a50e463e3dd5e2a47c811 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Wed, 11 Sep 2024 14:28:32 -0400 Subject: [PATCH 01/18] input error text --- src/common/components/Input/Input.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/components/Input/Input.tsx b/src/common/components/Input/Input.tsx index 026dd02..1c21626 100644 --- a/src/common/components/Input/Input.tsx +++ b/src/common/components/Input/Input.tsx @@ -29,6 +29,7 @@ interface InputProps const Input = forwardRef( ({ className, testid = 'input', ...props }: InputProps, ref): JSX.Element => { const [field, meta, helpers] = useField(props.name); + const errorText: string | undefined = meta.touched ? meta.error : undefined; return ( ( data-testid={testid} {...field} {...props} - errorText={meta.error} + errorText={errorText} ref={ref} > ); From 7fe015bdd07bef5aad3140cb0e7172bffceb1956 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Wed, 11 Sep 2024 14:28:54 -0400 Subject: [PATCH 02/18] initial DatetimeInput component --- .../components/Input/DatetimeInput.scss | 12 ++ src/common/components/Input/DatetimeInput.tsx | 132 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/common/components/Input/DatetimeInput.scss create mode 100644 src/common/components/Input/DatetimeInput.tsx diff --git a/src/common/components/Input/DatetimeInput.scss b/src/common/components/Input/DatetimeInput.scss new file mode 100644 index 0000000..b1f73df --- /dev/null +++ b/src/common/components/Input/DatetimeInput.scss @@ -0,0 +1,12 @@ +ion-input.ls-datetime-input { + input { + cursor: pointer; + } +} + +ion-modal.ls-datetime-modal { + --height: fit-content; + --width: fit-content; + + --border-radius: 0.5rem; +} diff --git a/src/common/components/Input/DatetimeInput.tsx b/src/common/components/Input/DatetimeInput.tsx new file mode 100644 index 0000000..d7361b1 --- /dev/null +++ b/src/common/components/Input/DatetimeInput.tsx @@ -0,0 +1,132 @@ +import { ComponentPropsWithoutRef, useState } from 'react'; +import { DatetimeCustomEvent, IonButton, IonDatetime, IonInput, IonModal } from '@ionic/react'; +import { useField } from 'formik'; +import classNames from 'classnames'; + +import './DatetimeInput.scss'; +import { PropsWithTestId } from '../types'; +import Input from './Input'; +import Icon, { IconName } from '../Icon/Icon'; +import dayjs from 'dayjs'; + +const DEFAULT_FORMAT_DATE: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', +}; + +type DatetimeValue = string | string[] | null; + +interface DatetimeInputProps + extends PropsWithTestId, + Pick, 'label' | 'labelPlacement'>, + Omit, 'name'>, + Required, 'name'>> {} + +const DatetimeInput = ({ + className, + label, + labelPlacement, + testid = 'input-datetime', + ...datetimeProps +}: DatetimeInputProps): JSX.Element => { + const [field, meta, helpers] = useField(datetimeProps.name); + const [isOpen, setIsOpen] = useState(false); + const [internalValue, setInternalValue] = useState(field.value); + + const errorText: string | undefined = meta.touched ? meta.error : undefined; + + console.log(`meta::${JSON.stringify(meta)}`); + + const onChange = async (e: DatetimeCustomEvent) => { + console.log(`DatetimeInput::onChange::value::${e.detail.value}`); + helpers.setTouched(true); + const value = e.detail.value; + if (value) { + if (Array.isArray(value)) { + setInternalValue(value.map((val) => dayjs(val).toISOString())); + } else { + setInternalValue(dayjs(value).toISOString()); + } + } else { + setInternalValue(undefined); + } + }; + + const onDidDismiss = async () => { + console.log(`DatetimeInput::onDidDismiss::value::${internalValue}`); + await helpers.setValue(internalValue); + setIsOpen(false); + }; + + const formatDate = (dateStr: DatetimeValue): string => { + if (dateStr) { + if (Array.isArray(dateStr)) { + // TODO format array of values + return 'array'; + } else { + return new Intl.DateTimeFormat(undefined, DEFAULT_FORMAT_DATE).format(new Date(dateStr)); + } + } + return ''; + }; + + const getLocalDatetime = (value: DatetimeValue | undefined): DatetimeValue => { + if (value) { + if (Array.isArray(value)) { + // TODO format array value + return ''; + } else { + return dayjs(value).format('YYYY-MM-DD[T]HH:mm'); + } + } else { + return null; + } + }; + + return ( + setIsOpen(true)} + errorText={errorText} + readonly + > + + + + + + + ); +}; + +export default DatetimeInput; From bb587e01e78eb971d5750bc8cfdfda6ccf077dd3 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Wed, 11 Sep 2024 14:42:13 -0400 Subject: [PATCH 03/18] user form field attrs --- src/pages/Users/components/UserForm/UserForm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/Users/components/UserForm/UserForm.tsx b/src/pages/Users/components/UserForm/UserForm.tsx index 3e79760..c3ac4a8 100644 --- a/src/pages/Users/components/UserForm/UserForm.tsx +++ b/src/pages/Users/components/UserForm/UserForm.tsx @@ -78,7 +78,6 @@ const UserForm = ({ label="Name" labelPlacement="stacked" disabled={isSubmitting} - required ref={focusInput} data-testid={`${testid}-field-name`} > @@ -87,7 +86,6 @@ const UserForm = ({ label="Username" labelPlacement="stacked" disabled={isSubmitting} - minlength={8} maxlength={30} data-testid={`${testid}-field-username`} > @@ -97,7 +95,6 @@ const UserForm = ({ label="Email" labelPlacement="stacked" disabled={isSubmitting} - required data-testid={`${testid}-field-email`} > Date: Wed, 11 Sep 2024 14:42:32 -0400 Subject: [PATCH 04/18] calendar icon --- src/common/components/Icon/Icon.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/common/components/Icon/Icon.tsx b/src/common/components/Icon/Icon.tsx index 123b35b..bd2af6f 100644 --- a/src/common/components/Icon/Icon.tsx +++ b/src/common/components/Icon/Icon.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { faBuilding, + faCalendar, faCircleInfo, faEnvelope, faHouse, @@ -42,6 +43,7 @@ export interface IconProps */ export enum IconName { Building = 'building', + Calendar = 'calendar', CircleInfo = 'circle_info', Envelope = 'envelope', House = 'house', @@ -65,6 +67,7 @@ export enum IconName { */ const icons: Record = { building: faBuilding, + calendar: faCalendar, circle_info: faCircleInfo, envelope: faEnvelope, house: faHouse, From 57bd91aedadc21ed6cbf8bcf206482e38eeeeec2 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Thu, 12 Sep 2024 06:42:19 -0400 Subject: [PATCH 05/18] datetimeinput internal event logic --- src/common/components/Input/DatetimeInput.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/common/components/Input/DatetimeInput.tsx b/src/common/components/Input/DatetimeInput.tsx index d7361b1..a9c041a 100644 --- a/src/common/components/Input/DatetimeInput.tsx +++ b/src/common/components/Input/DatetimeInput.tsx @@ -2,12 +2,12 @@ import { ComponentPropsWithoutRef, useState } from 'react'; import { DatetimeCustomEvent, IonButton, IonDatetime, IonInput, IonModal } from '@ionic/react'; import { useField } from 'formik'; import classNames from 'classnames'; +import dayjs from 'dayjs'; import './DatetimeInput.scss'; import { PropsWithTestId } from '../types'; import Input from './Input'; import Icon, { IconName } from '../Icon/Icon'; -import dayjs from 'dayjs'; const DEFAULT_FORMAT_DATE: Intl.DateTimeFormatOptions = { month: 'short', @@ -36,26 +36,31 @@ const DatetimeInput = ({ const errorText: string | undefined = meta.touched ? meta.error : undefined; - console.log(`meta::${JSON.stringify(meta)}`); + console.log(`DatetimeInput::field::meta::${JSON.stringify(meta)}`); const onChange = async (e: DatetimeCustomEvent) => { console.log(`DatetimeInput::onChange::value::${e.detail.value}`); - helpers.setTouched(true); const value = e.detail.value; if (value) { if (Array.isArray(value)) { setInternalValue(value.map((val) => dayjs(val).toISOString())); + await helpers.setValue( + value.map((val) => dayjs(val).toISOString()), + true, + ); } else { setInternalValue(dayjs(value).toISOString()); + await helpers.setValue(dayjs(value).toISOString(), true); } } else { setInternalValue(undefined); + await helpers.setValue(undefined, true); } }; const onDidDismiss = async () => { console.log(`DatetimeInput::onDidDismiss::value::${internalValue}`); - await helpers.setValue(internalValue); + await helpers.setTouched(true, true); setIsOpen(false); }; From 35cef42399144fd7a55aebbea4be59a850121efe Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Thu, 12 Sep 2024 06:43:30 -0400 Subject: [PATCH 06/18] profile dateOfBirth --- src/__fixtures__/profiles.ts | 1 + src/common/models/profile.ts | 1 + .../Account/components/Profile/ProfileForm.scss | 3 ++- .../Account/components/Profile/ProfileForm.tsx | 17 +++++++++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/__fixtures__/profiles.ts b/src/__fixtures__/profiles.ts index 83d347b..f57de99 100644 --- a/src/__fixtures__/profiles.ts +++ b/src/__fixtures__/profiles.ts @@ -4,4 +4,5 @@ export const profileFixture1: Profile = { name: 'Test User', email: 'test1@example.com', bio: 'My name is Test User.', + dateOfBirth: '2002-07-20T08:32:00-04:00', }; diff --git a/src/common/models/profile.ts b/src/common/models/profile.ts index 11fd134..261484c 100644 --- a/src/common/models/profile.ts +++ b/src/common/models/profile.ts @@ -5,4 +5,5 @@ import { User } from './user'; */ export type Profile = Pick & { bio?: string; + dateOfBirth?: string; }; diff --git a/src/pages/Account/components/Profile/ProfileForm.scss b/src/pages/Account/components/Profile/ProfileForm.scss index 728c291..182f8e7 100644 --- a/src/pages/Account/components/Profile/ProfileForm.scss +++ b/src/pages/Account/components/Profile/ProfileForm.scss @@ -1,5 +1,6 @@ .form-profile { - ion-input { + ion-input, + ion-textarea { margin-bottom: 0.5rem; } } diff --git a/src/pages/Account/components/Profile/ProfileForm.tsx b/src/pages/Account/components/Profile/ProfileForm.tsx index c4de8b4..7f493d0 100644 --- a/src/pages/Account/components/Profile/ProfileForm.tsx +++ b/src/pages/Account/components/Profile/ProfileForm.tsx @@ -15,6 +15,7 @@ import ErrorCard from 'common/components/Card/ErrorCard'; import Input from 'common/components/Input/Input'; import ButtonRow from 'common/components/Button/ButtonRow'; import Textarea from 'common/components/Input/Textarea'; +import DatetimeInput from 'common/components/Input/DatetimeInput'; /** * Profile form values. @@ -38,6 +39,7 @@ const validationSchema = object({ name: string().required('Required. '), email: string().required('Required. ').email('Must be an email address. '), bio: string().max(500, 'Must be 500 characters or less. '), + dateOfBirth: string().required('Required. ').datetime({ allowOffset: true }), }); /** @@ -81,6 +83,7 @@ const ProfileForm = ({ email: profile.email, name: profile.name, bio: profile.bio, + dateOfBirth: profile.dateOfBirth, }} onSubmit={(values, { setSubmitting }) => { setProgress(true); @@ -119,6 +122,7 @@ const ProfileForm = ({ ref={focusInput} data-testid={`${testid}-field-name`} /> + +