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:
parent
bcf9d119f6
commit
5ab2374ebb
4 changed files with 146 additions and 96 deletions
|
@ -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’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. “Retire at
|
|
||||||
45” sounds better than “Retire in 2043”.
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<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’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. “Retire at
|
||||||
|
45” sounds better than “Retire in 2043”.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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'}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
16
libs/client/shared/src/utils/profile-utils.ts
Normal file
16
libs/client/shared/src/utils/profile-utils.ts
Normal 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
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue