1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Issue #81: Date inputs should respect users date format

This commit is contained in:
Shivam0710 2024-01-18 03:18:20 +05:30
parent bcf9d119f6
commit 5ab2374ebb
4 changed files with 146 additions and 96 deletions

View file

@ -92,7 +92,7 @@ type ProfileViewProps = {
function ProfileForm({ title, onSubmit, defaultValues }: ProfileViewProps) { function ProfileForm({ title, onSubmit, defaultValues }: ProfileViewProps) {
const [currentQuestion, setCurrentQuestion] = useState< const [currentQuestion, setCurrentQuestion] = useState<
'birthday' | 'household' | 'residence' | 'goals' 'birthday' | 'household' | 'residence' | 'goals'
>('birthday') >('residence')
const { const {
control, control,
@ -122,98 +122,13 @@ function ProfileForm({ title, onSubmit, defaultValues }: ProfileViewProps) {
to building plans or receiving advice from our advisors. to building plans or receiving advice from our advisors.
</p> </p>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
<Question
open={currentQuestion === 'birthday'}
valid={!errors.dob}
icon={RiCakeLine}
label={<>When&rsquo;s your birthday?</>}
onClick={() => setCurrentQuestion('birthday')}
next={() => setCurrentQuestion('household')}
>
<Controller
control={control}
name="dob"
rules={{
validate: (d) =>
BrowserUtil.validateFormDate(d, {
minDate: DateTime.now().minus({ years: 100 }).toISODate(),
required: true,
}),
}}
render={({ field, fieldState: { error } }) => (
<DatePicker
popperPlacement="bottom"
className="mt-2"
minCalendarDate={DateTime.now().minus({ years: 100 }).toISODate()}
error={error?.message}
{...field}
/>
)}
/>
<Tooltip
placement="bottom-start"
content={
<>
We use your age to personalize plans to your context instead of
showing years when referring to future events. &ldquo;Retire at
45&rdquo; sounds better than &ldquo;Retire in 2043&rdquo;.
</>
}
>
<div className="flex items-center mt-4 text-base text-gray-50 cursor-default">
<RiQuestionLine className="w-5 h-5 mr-2 text-gray-100" />
Why do we need your age?
</div>
</Tooltip>
</Question>
<Question
open={currentQuestion === 'household'}
valid={!errors.household}
icon={RiHome5Line}
label="Which best describes your household?"
onClick={() => setCurrentQuestion('household')}
back={() => setCurrentQuestion('birthday')}
next={() => setCurrentQuestion('residence')}
>
<div className="space-y-2">
<Controller
control={control}
name="household"
rules={{ required: true }}
render={({ field }) => (
<>
{Object.entries({
single: 'Single income, no dependents',
singleWithDependents:
'Single income, at least one dependent',
dual: 'Dual income, no dependents',
dualWithDependents: 'Dual income, at least one dependent',
retired: 'Retired or financially independent',
}).map(([value, label]) => (
<Checkbox
key={value}
label={label}
checked={field.value === value}
onChange={(checked) => {
if (checked) field.onChange(value)
}}
/>
))}
</>
)}
/>
</div>
</Question>
<Question <Question
open={currentQuestion === 'residence'} open={currentQuestion === 'residence'}
valid={!errors.country} valid={!errors.country}
icon={RiMapPin2Line} icon={RiMapPin2Line}
label="Where are you based?" label="Where are you based?"
onClick={() => setCurrentQuestion('residence')} onClick={() => setCurrentQuestion('residence')}
back={() => setCurrentQuestion('household')} next={() => setCurrentQuestion('birthday')}
next={() => setCurrentQuestion('goals')}
> >
<div className="space-y-2"> <div className="space-y-2">
<Controller <Controller
@ -253,6 +168,94 @@ function ProfileForm({ title, onSubmit, defaultValues }: ProfileViewProps) {
</div> </div>
</Tooltip> </Tooltip>
</Question> </Question>
<Question
open={currentQuestion === 'birthday'}
valid={!errors.dob}
icon={RiCakeLine}
label={<>When&rsquo;s your birthday?</>}
onClick={() => setCurrentQuestion('birthday')}
next={() => setCurrentQuestion('household')}
back={() => setCurrentQuestion('residence')}
>
<Controller
control={control}
name="dob"
rules={{
validate: (d) =>
BrowserUtil.validateFormDate(d, {
minDate: DateTime.now().minus({ years: 100 }).toISODate(),
required: true,
}),
}}
render={({ field, fieldState: { error } }) => (
<DatePicker
popperPlacement="bottom"
className="mt-2"
minCalendarDate={DateTime.now().minus({ years: 100 }).toISODate()}
error={error?.message}
placeholder={BrowserUtil.getDateFormatByCountryCode(
country
).toUpperCase()}
dateFormat={BrowserUtil.getDateFormatByCountryCode(country)}
{...field}
/>
)}
/>
<Tooltip
placement="bottom-start"
content={
<>
We use your age to personalize plans to your context instead of
showing years when referring to future events. &ldquo;Retire at
45&rdquo; sounds better than &ldquo;Retire in 2043&rdquo;.
</>
}
>
<div className="flex items-center mt-4 text-base text-gray-50 cursor-default">
<RiQuestionLine className="w-5 h-5 mr-2 text-gray-100" />
Why do we need your age?
</div>
</Tooltip>
</Question>
<Question
open={currentQuestion === 'household'}
valid={!errors.household}
icon={RiHome5Line}
label="Which best describes your household?"
onClick={() => setCurrentQuestion('household')}
back={() => setCurrentQuestion('birthday')}
next={() => setCurrentQuestion('goals')}
>
<div className="space-y-2">
<Controller
control={control}
name="household"
rules={{ required: true }}
render={({ field }) => (
<>
{Object.entries({
single: 'Single income, no dependents',
singleWithDependents:
'Single income, at least one dependent',
dual: 'Dual income, no dependents',
dualWithDependents: 'Dual income, at least one dependent',
retired: 'Retired or financially independent',
}).map(([value, label]) => (
<Checkbox
key={value}
label={label}
checked={field.value === value}
onChange={(checked) => {
if (checked) field.onChange(value)
}}
/>
))}
</>
)}
/>
</div>
</Question>
<Question <Question
open={currentQuestion === 'goals'} open={currentQuestion === 'goals'}

View file

@ -2,3 +2,4 @@ export * from './image-loaders'
export * from './browser-utils' export * from './browser-utils'
export * from './account-utils' export * from './account-utils'
export * from './form-utils' export * from './form-utils'
export * from './profile-utils'

View file

@ -0,0 +1,16 @@
export function getDateFormatByCountryCode(countryCode: string): string {
let dateFormat = ''
switch (countryCode.toLowerCase()) {
case 'in':
dateFormat = 'dd / MM / yyyy'
break
case 'us':
dateFormat = 'yyyy / MM / dd'
break
default:
dateFormat = 'dd / MM / yyyy'
break
}
return dateFormat
}

View file

@ -11,7 +11,7 @@ import { usePopper } from 'react-popper'
import { DatePickerCalendar } from './DatePickerCalendar' import { DatePickerCalendar } from './DatePickerCalendar'
import { MAX_SUPPORTED_DATE, MIN_SUPPORTED_DATE } from './utils' import { MAX_SUPPORTED_DATE, MIN_SUPPORTED_DATE } from './utils'
const INPUT_DATE_FORMAT = 'MM / dd / yyyy' const DEFAULT_INPUT_DATE_FORMAT = 'MM / dd / yyyy'
export interface DatePickerProps { export interface DatePickerProps {
name: string name: string
@ -25,11 +25,41 @@ export interface DatePickerProps {
maxCalendarDate?: string maxCalendarDate?: string
popperPlacement?: PopperJs.Placement popperPlacement?: PopperJs.Placement
popperStrategy?: PopperJs.PositioningStrategy popperStrategy?: PopperJs.PositioningStrategy
dateFormat?: string
} }
function toFormattedStr(date: string | null) { function toFormattedStr(date: string | null, dateFormat: string): string {
if (!date) return '' if (!date) return ''
return DateTime.fromISO(date).toFormat(INPUT_DATE_FORMAT) return DateTime.fromISO(date).toFormat(dateFormat)
}
// function to get mask from the given format
function getMaskArray(dateFormat: String): string[] {
return dateFormat
.split('/')
.map((keyword) => keyword.trim())
.join('')
.split('')
.map((keyword) => keyword.toUpperCase())
}
function getDateFormatPattern(dateFormat: string): string {
const dateComponents = dateFormat.split('/')
const pattern = dateComponents
.map((component) => {
if (component.includes('d')) {
return ' ## ' // Placeholder for day
} else if (component.includes('m') || component.includes('M')) {
return ' ## ' // Placeholder for month
} else if (component.includes('y')) {
return ' #### ' // Placeholder for year
}
return component // Keep non-date components unchanged
})
.join('/')
.trim()
return pattern
} }
function DatePicker( function DatePicker(
@ -45,6 +75,7 @@ function DatePicker(
maxCalendarDate = MAX_SUPPORTED_DATE.toISODate(), maxCalendarDate = MAX_SUPPORTED_DATE.toISODate(),
popperPlacement = 'auto', popperPlacement = 'auto',
popperStrategy = 'fixed', popperStrategy = 'fixed',
dateFormat = DEFAULT_INPUT_DATE_FORMAT,
}: DatePickerProps, }: DatePickerProps,
ref: Ref<HTMLInputElement> ref: Ref<HTMLInputElement>
): JSX.Element { ): JSX.Element {
@ -79,15 +110,14 @@ function DatePicker(
setCalendarValue('') setCalendarValue('')
onChange(null) onChange(null)
} else { } else {
const inputDate = DateTime.fromFormat(date.formattedValue, INPUT_DATE_FORMAT) const inputDate = DateTime.fromFormat(date.formattedValue, dateFormat)
if (inputDate.isValid) { if (inputDate.isValid) {
setCalendarValue(inputDate.toISODate()) setCalendarValue(inputDate.toISODate())
onChange(inputDate.toISODate()) onChange(inputDate.toISODate())
} }
} }
}, },
[onChange] [onChange, dateFormat]
) )
return ( return (
@ -97,10 +127,10 @@ function DatePicker(
name={name} name={name}
customInput={Input} // passes all props below to <Input /> - https://github.com/s-yadav/react-number-format#custom-inputs customInput={Input} // passes all props below to <Input /> - https://github.com/s-yadav/react-number-format#custom-inputs
getInputRef={ref} getInputRef={ref}
format="## / ## / ####" format={getDateFormatPattern(dateFormat)}
placeholder={placeholder} placeholder={placeholder}
mask={['M', 'M', 'D', 'D', 'Y', 'Y', 'Y', 'Y']} mask={getMaskArray(dateFormat)}
value={toFormattedStr(value)} value={toFormattedStr(value, dateFormat)}
error={error} error={error}
label={label} label={label}
onValueChange={handleInputValueChange} onValueChange={handleInputValueChange}