Feature/wrap logging * feat: change all logging to go through our own logger function so that we can control log levels * move packages/trpc to using our own logger * merge Approved-by: Linus Flood
193 lines
5.6 KiB
TypeScript
193 lines
5.6 KiB
TypeScript
"use client"
|
|
import { parseDate } from "@internationalized/date"
|
|
import { useEffect } from "react"
|
|
import { useController, useFormContext, useWatch } from "react-hook-form"
|
|
import { useIntl } from "react-intl"
|
|
import { useMediaQuery } from "usehooks-ts"
|
|
|
|
import { dt } from "@scandic-hotels/common/dt"
|
|
import { logger } from "@scandic-hotels/common/logger"
|
|
import { Select } from "@scandic-hotels/design-system/Select"
|
|
|
|
import useLang from "@/hooks/useLang"
|
|
import { getLocalizedMonthName } from "@/utils/dateFormatting"
|
|
import { rangeArray } from "@/utils/rangeArray"
|
|
|
|
import ErrorMessage from "../ErrorMessage"
|
|
import { DateName, type DateProps } from "./date"
|
|
|
|
import styles from "./date.module.css"
|
|
|
|
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
|
const intl = useIntl()
|
|
const lang = useLang()
|
|
const isDesktop = useMediaQuery("(min-width: 768px)", {
|
|
initializeWithValue: false,
|
|
})
|
|
|
|
const { control, setValue, formState, watch } = useFormContext()
|
|
const { field, fieldState } = useController({
|
|
control,
|
|
name,
|
|
rules: registerOptions,
|
|
})
|
|
|
|
const currentDateValue: string = useWatch({ name })
|
|
const year = watch(DateName.year)
|
|
const month = watch(DateName.month)
|
|
const day = watch(DateName.day)
|
|
|
|
const minAgeDate = dt().subtract(18, "year").toDate() // age 18
|
|
const minAgeYear = minAgeDate.getFullYear()
|
|
const minAgeMonth = year === minAgeYear ? minAgeDate.getMonth() + 1 : null
|
|
const minAgeDay =
|
|
Number(year) === minAgeYear && Number(month) === minAgeMonth
|
|
? minAgeDate.getDate()
|
|
: null
|
|
|
|
const months = rangeArray(1, minAgeMonth ?? 12).map((month) => ({
|
|
value: month,
|
|
label: getLocalizedMonthName(month, lang),
|
|
}))
|
|
|
|
const years = rangeArray(1900, minAgeYear)
|
|
.reverse()
|
|
.map((year) => ({ value: year, label: year.toString() }))
|
|
|
|
// Calculate available days based on selected year and month
|
|
const daysInMonth = getDaysInMonth(
|
|
year ? Number(year) : null,
|
|
month ? Number(month) - 1 : null
|
|
)
|
|
|
|
const days = rangeArray(1, minAgeDay ?? daysInMonth).map((day) => ({
|
|
value: day,
|
|
label: `${day}`,
|
|
}))
|
|
|
|
useEffect(() => {
|
|
if (formState.isSubmitting) return
|
|
|
|
if (month && day) {
|
|
const maxDays = getDaysInMonth(
|
|
year ? Number(year) : null,
|
|
Number(month) - 1
|
|
)
|
|
const adjustedDay = Number(day) > maxDays ? maxDays : Number(day)
|
|
|
|
if (adjustedDay !== Number(day)) {
|
|
setValue(DateName.day, adjustedDay)
|
|
}
|
|
}
|
|
|
|
const newDate = dt()
|
|
.year(Number(year))
|
|
.month(Number(month) - 1)
|
|
.date(Number(day))
|
|
|
|
if (newDate.isValid()) {
|
|
setValue(name, newDate.format("YYYY-MM-DD"), {
|
|
shouldDirty: true,
|
|
shouldTouch: true,
|
|
shouldValidate: true,
|
|
})
|
|
}
|
|
}, [year, month, day, setValue, name, formState.isSubmitting])
|
|
|
|
let dateValue = null
|
|
try {
|
|
/**
|
|
* parseDate throws when its not a valid
|
|
* date, but we can't check isNan since
|
|
* we recieve the date as "1999-01-01"
|
|
*/
|
|
dateValue = dt(currentDateValue).isValid()
|
|
? parseDate(currentDateValue)
|
|
: null
|
|
} catch (error) {
|
|
logger.warn("Known error for parse date in DateSelect: ", error)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (formState.isSubmitting) return
|
|
|
|
if (!(day && month && year) && dateValue) {
|
|
setValue(DateName.day, Number(dateValue.day))
|
|
setValue(DateName.month, Number(dateValue.month))
|
|
setValue(DateName.year, Number(dateValue.year))
|
|
}
|
|
}, [setValue, formState.isSubmitting, dateValue, day, month, year])
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.container}>
|
|
<div className={styles.segment}>
|
|
<Select
|
|
items={days}
|
|
label={intl.formatMessage({
|
|
defaultMessage: "Day",
|
|
})}
|
|
name={DateName.day}
|
|
onSelectionChange={(key) => setValue(DateName.day, Number(key))}
|
|
isRequired
|
|
enableFiltering={isDesktop}
|
|
isInvalid={fieldState.invalid}
|
|
onBlur={field.onBlur}
|
|
defaultSelectedKey={dateValue?.day}
|
|
inputMode="numeric"
|
|
data-testid={DateName.day}
|
|
/>
|
|
</div>
|
|
<div className={styles.segment}>
|
|
<Select
|
|
items={months}
|
|
label={intl.formatMessage({
|
|
defaultMessage: "Month",
|
|
})}
|
|
name={DateName.month}
|
|
onSelectionChange={(key) => setValue(DateName.month, Number(key))}
|
|
isRequired
|
|
enableFiltering={isDesktop}
|
|
isInvalid={fieldState.invalid}
|
|
onBlur={field.onBlur}
|
|
defaultSelectedKey={dateValue?.month}
|
|
data-testid={DateName.month}
|
|
/>
|
|
</div>
|
|
<div className={styles.segment}>
|
|
<Select
|
|
items={years}
|
|
label={intl.formatMessage({
|
|
defaultMessage: "Year",
|
|
})}
|
|
name={DateName.year}
|
|
onSelectionChange={(key) => setValue(DateName.year, Number(key))}
|
|
isRequired
|
|
enableFiltering={isDesktop}
|
|
isInvalid={fieldState.invalid}
|
|
onBlur={field.onBlur}
|
|
defaultSelectedKey={dateValue?.year}
|
|
inputMode="numeric"
|
|
data-testid={DateName.year}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<ErrorMessage errors={formState.errors} name={field.name} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
function getDaysInMonth(year: number | null, month: number | null): number {
|
|
if (month === null) {
|
|
return 31
|
|
}
|
|
|
|
// If month is February and no year selected, return minimum.
|
|
if (month === 1 && !year) {
|
|
return 28
|
|
}
|
|
|
|
const yearToUse = year ?? new Date().getFullYear()
|
|
return dt(`${yearToUse}-${month + 1}-01`).daysInMonth()
|
|
}
|