Merged in chore/SW-3145-move-date-to-design-system (pull request #2556)
feat: SW-3145 Moved date component to design system * chore: SW-3145 Moved date component to design system Approved-by: Anton Gunnarsson Approved-by: Matilda Landström
This commit is contained in:
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import CountrySelect from "@scandic-hotels/design-system/Form/Country"
|
||||
import DateSelect from "@scandic-hotels/design-system/Form/Date"
|
||||
import Phone from "@scandic-hotels/design-system/Form/Phone"
|
||||
import { Select } from "@scandic-hotels/design-system/Select"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
getLocalizedLanguageOptions,
|
||||
} from "@/constants/languages"
|
||||
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
||||
import useLang from "@/hooks/useLang"
|
||||
@@ -41,7 +41,20 @@ export default function FormContent({ errors }: { errors: FieldErrors }) {
|
||||
</p>
|
||||
</Typography>
|
||||
</header>
|
||||
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
|
||||
<DateSelect
|
||||
labels={{
|
||||
day: intl.formatMessage({ defaultMessage: "Day" }),
|
||||
month: intl.formatMessage({ defaultMessage: "Month" }),
|
||||
year: intl.formatMessage({ defaultMessage: "Year" }),
|
||||
errorMessage: getErrorMessage(
|
||||
intl,
|
||||
errors["dateOfBirth"]?.message?.toString()
|
||||
),
|
||||
}}
|
||||
lang={lang}
|
||||
name="dateOfBirth"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
label={`${intl.formatMessage({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { logger } from "@scandic-hotels/common/logger"
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import CountrySelect from "@scandic-hotels/design-system/Form/Country"
|
||||
import DateSelect from "@scandic-hotels/design-system/Form/Date"
|
||||
import Phone from "@scandic-hotels/design-system/Form/Phone"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trpc } from "@scandic-hotels/trpc/client"
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
privacyPolicy,
|
||||
} from "@/constants/webHrefs"
|
||||
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import PasswordInput from "@/components/TempDesignSystem/Form/PasswordInput"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
@@ -162,6 +162,16 @@ export default function SignupForm({ title }: SignUpFormProps) {
|
||||
</p>
|
||||
</Typography>
|
||||
<DateSelect
|
||||
labels={{
|
||||
day: intl.formatMessage({ defaultMessage: "Day" }),
|
||||
month: intl.formatMessage({ defaultMessage: "Month" }),
|
||||
year: intl.formatMessage({ defaultMessage: "Year" }),
|
||||
errorMessage: getErrorMessage(
|
||||
intl,
|
||||
errors.dateOfBirth?.message?.toString()
|
||||
),
|
||||
}}
|
||||
lang={lang}
|
||||
name="dateOfBirth"
|
||||
registerOptions={{ required: true }}
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { type RegisterOptions, useWatch } from "react-hook-form"
|
||||
import {
|
||||
type FieldErrors,
|
||||
type RegisterOptions,
|
||||
useWatch,
|
||||
} from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Caption from "@scandic-hotels/design-system/Caption"
|
||||
import DateSelect from "@scandic-hotels/design-system/Form/Date"
|
||||
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getErrorMessage } from "@/utils/getErrorMessage"
|
||||
|
||||
import styles from "./signup.module.css"
|
||||
|
||||
export default function Signup({
|
||||
errors,
|
||||
name,
|
||||
registerOptions,
|
||||
}: {
|
||||
errors: FieldErrors
|
||||
name: string
|
||||
registerOptions?: RegisterOptions
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
const [isJoinChecked, setIsJoinChecked] = useState(false)
|
||||
|
||||
@@ -50,6 +59,16 @@ export default function Signup({
|
||||
</Caption>
|
||||
</header>
|
||||
<DateSelect
|
||||
labels={{
|
||||
day: intl.formatMessage({ defaultMessage: "Day" }),
|
||||
month: intl.formatMessage({ defaultMessage: "Month" }),
|
||||
year: intl.formatMessage({ defaultMessage: "Year" }),
|
||||
errorMessage: getErrorMessage(
|
||||
intl,
|
||||
errors["dateOfBirth"]?.message?.toString()
|
||||
),
|
||||
}}
|
||||
lang={lang}
|
||||
name="dateOfBirth"
|
||||
registerOptions={{ required: true, ...registerOptions }}
|
||||
/>
|
||||
|
||||
@@ -214,6 +214,7 @@ export default function Details({ user }: DetailsProps) {
|
||||
{user ? null : (
|
||||
<div className={styles.fullWidth}>
|
||||
<Signup
|
||||
errors={formState.errors}
|
||||
name="join"
|
||||
registerOptions={{ onBlur: updateDetailsStore }}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { getNumberOfNights } from "@scandic-hotels/common/utils/dateFormatting"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||
|
||||
@@ -6,7 +7,6 @@ import { longDateWithYearFormat } from "@/constants/dateFormats"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { getNumberOfNights } from "@/utils/dateFormatting"
|
||||
|
||||
import styles from "./footer.module.css"
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media screen and (width < 400px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.segment {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { RegisterOptions } from "react-hook-form"
|
||||
|
||||
export const enum DateName {
|
||||
date = "date",
|
||||
day = "day",
|
||||
month = "month",
|
||||
year = "year",
|
||||
}
|
||||
export interface DateProps
|
||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
name: string
|
||||
registerOptions?: RegisterOptions
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
"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 { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage"
|
||||
import { Select } from "@scandic-hotels/design-system/Select"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getLocalizedMonthName } from "@/utils/dateFormatting"
|
||||
import { getErrorMessage } from "@/utils/getErrorMessage"
|
||||
import { rangeArray } from "@/utils/rangeArray"
|
||||
|
||||
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}
|
||||
messageLabel={getErrorMessage(
|
||||
intl,
|
||||
formState.errors[field.name]?.message?.toString()
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -7,11 +7,10 @@ import { afterEach, describe, expect, test } from "vitest"
|
||||
|
||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { getLocalizedMonthName } from "@scandic-hotels/common/utils/dateFormatting"
|
||||
import Date from "@scandic-hotels/design-system/Form/Date"
|
||||
|
||||
import { cleanup, render, screen } from "@/tests/utils"
|
||||
import { getLocalizedMonthName } from "@/utils/dateFormatting"
|
||||
|
||||
import Date from "./index"
|
||||
|
||||
const testUtilUser = new User({ interactionType: "touch" })
|
||||
|
||||
@@ -116,7 +115,16 @@ describe("Date input", () => {
|
||||
expect(data).toEqual(expectedOutput)
|
||||
}}
|
||||
>
|
||||
<Date name="dateOfBirth" />
|
||||
<Date
|
||||
labels={{
|
||||
day: "day",
|
||||
month: "Month",
|
||||
year: "Year",
|
||||
errorMessage: "Date is required",
|
||||
}}
|
||||
lang={Lang.en}
|
||||
name="dateOfBirth"
|
||||
/>
|
||||
</FormWrapper>
|
||||
)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
|
||||
/**
|
||||
* Get the localized month name for a given month index and language
|
||||
* @param monthIndex - The month index (1-12)
|
||||
* @param lang - the language to use, Lang enum
|
||||
* @returns The localized month name
|
||||
*/
|
||||
export function getLocalizedMonthName(monthIndex: number, lang: Lang) {
|
||||
const monthName = new Date(2024, monthIndex - 1).toLocaleString(lang, {
|
||||
month: "long",
|
||||
})
|
||||
|
||||
return monthName.charAt(0).toUpperCase() + monthName.slice(1)
|
||||
}
|
||||
|
||||
export function getNumberOfNights(startDate: string, endDate: string) {
|
||||
return dt(endDate).diff(startDate, "day")
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export function rangeArray(start: number, stop: number, step: number = 1) {
|
||||
return Array.from(
|
||||
{ length: (stop - start) / step + 1 },
|
||||
(value, index) => start + index * step
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user