fix: validation

This commit is contained in:
Christel Westerberg
2024-11-14 11:41:56 +01:00
parent 7a3194f978
commit 9189d588d1
6 changed files with 120 additions and 94 deletions

View File

@@ -1,18 +1,37 @@
import { z } from "zod" import { z } from "zod"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
import type { Location } from "@/types/trpc/routers/hotel/locations" import type { Location } from "@/types/trpc/routers/hotel/locations"
export const guestRoomSchema = z.object({ export const guestRoomSchema = z
.object({
adults: z.number().default(1), adults: z.number().default(1),
child: z child: z
.array( .array(
z.object({ z.object({
age: z.number().nonnegative(), age: z.number().min(0, "Age is required"),
bed: z.number(), bed: z.number().min(0, "Bed choice is required"),
}) })
) )
.default([]), .default([]),
}) })
.superRefine((value, ctx) => {
const childrenInAdultsBed = value.child.filter(
(c) => c.bed === ChildBedMapEnum.IN_ADULTS_BED
)
if (value.adults < childrenInAdultsBed.length) {
const lastAdultBedIndex = value.child
.map((c) => c.bed)
.lastIndexOf(ChildBedMapEnum.IN_ADULTS_BED)
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
"You cannot have more children in adults bed than adults in the room",
path: ["child", lastAdultBedIndex],
})
}
})
export const guestRoomsSchema = z.array(guestRoomSchema) export const guestRoomsSchema = z.array(guestRoomSchema)

View File

@@ -9,44 +9,26 @@ import Counter from "../Counter"
import styles from "./adult-selector.module.css" import styles from "./adult-selector.module.css"
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
import {
Child,
SelectorProps,
} from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function AdultSelector({ export default function AdultSelector({
roomIndex = 0, roomIndex = 0,
currentAdults, currentAdults,
currentChildren,
childrenInAdultsBed,
}: SelectorProps) { }: SelectorProps) {
const name = `rooms.${roomIndex}.adults`
const intl = useIntl() const intl = useIntl()
const adultsLabel = intl.formatMessage({ id: "Adults" }) const adultsLabel = intl.formatMessage({ id: "Adults" })
const { setValue } = useFormContext() const { setValue } = useFormContext()
function increaseAdultsCount(roomIndex: number) { function increaseAdultsCount() {
if (currentAdults < 6) { if (currentAdults < 6) {
setValue(`rooms.${roomIndex}.adults`, currentAdults + 1) setValue(name, currentAdults + 1)
} }
} }
function decreaseAdultsCount(roomIndex: number) { function decreaseAdultsCount() {
if (currentAdults > 1) { if (currentAdults > 1) {
setValue(`rooms.${roomIndex}.adults`, currentAdults - 1) setValue(name, currentAdults - 1)
if (childrenInAdultsBed > currentAdults) {
const toUpdateIndex = currentChildren.findIndex(
(child: Child) => child.bed == ChildBedMapEnum.IN_ADULTS_BED
)
if (toUpdateIndex != -1) {
setValue(
`rooms.${roomIndex}.children.${toUpdateIndex}.bed`,
currentChildren[toUpdateIndex].age < 3
? ChildBedMapEnum.IN_CRIB
: ChildBedMapEnum.IN_EXTRA_BED
)
}
}
} }
} }
@@ -57,12 +39,8 @@ export default function AdultSelector({
</Caption> </Caption>
<Counter <Counter
count={currentAdults} count={currentAdults}
handleOnDecrease={() => { handleOnDecrease={decreaseAdultsCount}
decreaseAdultsCount(roomIndex) handleOnIncrease={increaseAdultsCount}
}}
handleOnIncrease={() => {
increaseAdultsCount(roomIndex)
}}
disableDecrease={currentAdults == 1} disableDecrease={currentAdults == 1}
disableIncrease={currentAdults == 6} disableIncrease={currentAdults == 6}
/> />

View File

@@ -15,9 +15,9 @@ import {
ChildInfoSelectorProps, ChildInfoSelectorProps,
} from "@/types/components/bookingWidget/guestsRoomsPicker" } from "@/types/components/bookingWidget/guestsRoomsPicker"
const ageList = Array.from(Array(13).keys()).map((age) => ({ const ageList = [...Array(13)].map((_, i) => ({
label: age.toString(), label: i.toString(),
value: age, value: i,
})) }))
export default function ChildInfoSelector({ export default function ChildInfoSelector({
@@ -27,22 +27,23 @@ export default function ChildInfoSelector({
index = 0, index = 0,
roomIndex = 0, roomIndex = 0,
}: ChildInfoSelectorProps) { }: ChildInfoSelectorProps) {
const ageFieldName = `rooms.${roomIndex}.child.${index}.age`
const bedFieldName = `rooms.${roomIndex}.child.${index}.bed`
const intl = useIntl() const intl = useIntl()
const ageLabel = intl.formatMessage({ id: "Age" }) const ageLabel = intl.formatMessage({ id: "Age" })
const ageReqdErrMsg = intl.formatMessage({ id: "Child age is required" })
const bedLabel = intl.formatMessage({ id: "Bed" }) const bedLabel = intl.formatMessage({ id: "Bed" })
const { setValue, formState } = useFormContext() const errorMessage = intl.formatMessage({ id: "Child age is required" })
const { setValue, formState, register, trigger } = useFormContext()
function updateSelectedAge(age: number) {
setValue(`rooms.${roomIndex}.child.${index}.age`, age, {
shouldValidate: true,
})
const availableBedTypes = getAvailableBeds(age)
updateSelectedBed(availableBedTypes[0].value)
}
function updateSelectedBed(bed: number) { function updateSelectedBed(bed: number) {
setValue(`rooms.${roomIndex}.child.${index}.bed`, bed) setValue(`rooms.${roomIndex}.child.${index}.bed`, bed)
trigger()
}
function updateSelectedAge(age: number) {
setValue(`rooms.${roomIndex}.child.${index}.age`, age)
const availableBedTypes = getAvailableBeds(age)
updateSelectedBed(availableBedTypes[0].value)
} }
const allBedTypes: ChildBed[] = [ const allBedTypes: ChildBed[] = [
@@ -76,6 +77,10 @@ export default function ChildInfoSelector({
//@ts-expect-error: formState is typed with FormValues //@ts-expect-error: formState is typed with FormValues
const roomErrors = formState.errors.rooms?.[roomIndex]?.child?.[index] const roomErrors = formState.errors.rooms?.[roomIndex]?.child?.[index]
const ageError = roomErrors?.age
const bedError = roomErrors?.bed
return ( return (
<> <>
<div key={index} className={styles.childInfoContainer}> <div key={index} className={styles.childInfoContainer}>
@@ -89,13 +94,15 @@ export default function ChildInfoSelector({
onSelect={(key) => { onSelect={(key) => {
updateSelectedAge(key as number) updateSelectedAge(key as number)
}} }}
name={`rooms.${roomIndex}.child.${index}.age`}
placeholder={ageLabel} placeholder={ageLabel}
maxHeight={150} maxHeight={150}
{...register(ageFieldName, {
required: true,
})}
/> />
</div> </div>
<div> <div>
{child.age !== -1 ? ( {child.age >= 0 ? (
<Select <Select
items={getAvailableBeds(child.age)} items={getAvailableBeds(child.age)}
label={bedLabel} label={bedLabel}
@@ -104,17 +111,26 @@ export default function ChildInfoSelector({
onSelect={(key) => { onSelect={(key) => {
updateSelectedBed(key as number) updateSelectedBed(key as number)
}} }}
name={`rooms.${roomIndex}.child.${index}.age`}
placeholder={bedLabel} placeholder={bedLabel}
{...register(bedFieldName, {
required: true,
})}
/> />
) : null} ) : null}
</div> </div>
</div> </div>
{roomErrors ? ( {roomErrors && roomErrors.message ? (
<Caption color="red" className={styles.error}> <Caption color="red" className={styles.error}>
<ErrorCircleIcon color="red" /> <ErrorCircleIcon color="red" />
{ageReqdErrMsg} {roomErrors.message}
</Caption>
) : null}
{ageError || bedError ? (
<Caption color="red" className={styles.error}>
<ErrorCircleIcon color="red" />
{errorMessage}
</Caption> </Caption>
) : null} ) : null}
</> </>

View File

@@ -10,7 +10,6 @@ import ChildInfoSelector from "./ChildInfoSelector"
import styles from "./child-selector.module.css" import styles from "./child-selector.module.css"
import { BookingWidgetSchema } from "@/types/components/bookingWidget"
import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker" import { SelectorProps } from "@/types/components/bookingWidget/guestsRoomsPicker"
export default function ChildSelector({ export default function ChildSelector({
@@ -21,26 +20,20 @@ export default function ChildSelector({
}: SelectorProps) { }: SelectorProps) {
const intl = useIntl() const intl = useIntl()
const childrenLabel = intl.formatMessage({ id: "Children" }) const childrenLabel = intl.formatMessage({ id: "Children" })
const { setValue } = useFormContext<BookingWidgetSchema>() const { setValue } = useFormContext()
function increaseChildrenCount(roomIndex: number) { function increaseChildrenCount(roomIndex: number) {
if (currentChildren.length < 5) { if (currentChildren.length < 5) {
setValue( setValue(`rooms.${roomIndex}.child.${currentChildren.length}`, {
`rooms.${roomIndex}.child.${currentChildren.length}`, age: undefined,
{ bed: undefined,
age: -1, })
bed: -1,
},
{ shouldValidate: true }
)
} }
} }
function decreaseChildrenCount(roomIndex: number) { function decreaseChildrenCount(roomIndex: number) {
if (currentChildren.length > 0) { if (currentChildren.length > 0) {
currentChildren.pop() currentChildren.pop()
setValue(`rooms.${roomIndex}.child`, currentChildren, { setValue(`rooms.${roomIndex}.child`, currentChildren)
shouldValidate: true,
})
} }
} }

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useFormContext } from "react-hook-form" import { useEffect } from "react"
import { useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons" import { CloseLargeIcon, PlusCircleIcon, PlusIcon } from "../Icons"
@@ -35,7 +36,24 @@ export default function GuestsRoomsPickerDialog({
}) })
const addRoomLabel = intl.formatMessage({ id: "Add Room" }) const addRoomLabel = intl.formatMessage({ id: "Add Room" })
const { getFieldState } = useFormContext<BookingWidgetSchema>() const { getFieldState, trigger } = useFormContext<BookingWidgetSchema>()
const roomsValue = useWatch({ name: "rooms" })
async function handleOnClose() {
const state = await trigger("rooms")
if (state) {
onClose()
}
}
const fieldState = getFieldState("rooms")
useEffect(() => {
if (fieldState.invalid) {
trigger("rooms")
}
}, [roomsValue, fieldState.invalid, trigger])
return ( return (
<> <>
@@ -124,7 +142,7 @@ export default function GuestsRoomsPickerDialog({
</Tooltip> </Tooltip>
</div> </div>
<Button <Button
onPress={onClose} onPress={handleOnClose}
disabled={getFieldState("rooms").invalid} disabled={getFieldState("rooms").invalid}
className={styles.hideOnMobile} className={styles.hideOnMobile}
intent="tertiary" intent="tertiary"
@@ -134,7 +152,7 @@ export default function GuestsRoomsPickerDialog({
{doneLabel} {doneLabel}
</Button> </Button>
<Button <Button
onPress={onClose} onPress={handleOnClose}
disabled={getFieldState("rooms").invalid} disabled={getFieldState("rooms").invalid}
className={styles.hideOnDesktop} className={styles.hideOnDesktop}
intent="tertiary" intent="tertiary"

View File

@@ -9,6 +9,7 @@ import {
} from "react-aria-components" } from "react-aria-components"
import { useFormContext } from "react-hook-form" import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
@@ -22,6 +23,8 @@ export default function GuestsRoomsPickerForm() {
const { watch } = useFormContext() const { watch } = useFormContext()
const rooms = watch("rooms") as GuestsRoom[] const rooms = watch("rooms") as GuestsRoom[]
const isDesktop = useMediaQuery("(min-width: 1367px)")
const htmlElement = const htmlElement =
typeof window !== "undefined" ? document.querySelector("body") : null typeof window !== "undefined" ? document.querySelector("body") : null
//isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed". //isOpen is the 'old state', so isOpen === true means "The modal is open and WILL be closed".
@@ -37,16 +40,7 @@ export default function GuestsRoomsPickerForm() {
} }
} }
return ( return isDesktop ? (
<>
<DialogTrigger>
<Trigger rooms={rooms} className={styles.triggerMobile} />
<Modal>
<Dialog className={styles.pickerContainerMobile}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
<DialogTrigger onOpenChange={setOverflowClip}> <DialogTrigger onOpenChange={setOverflowClip}>
<Trigger rooms={rooms} className={styles.triggerDesktop} /> <Trigger rooms={rooms} className={styles.triggerDesktop} />
<Popover placement="bottom start" offset={36}> <Popover placement="bottom start" offset={36}>
@@ -55,7 +49,15 @@ export default function GuestsRoomsPickerForm() {
</Dialog> </Dialog>
</Popover> </Popover>
</DialogTrigger> </DialogTrigger>
</> ) : (
<DialogTrigger>
<Trigger rooms={rooms} className={styles.triggerMobile} />
<Modal>
<Dialog className={styles.pickerContainerMobile}>
{({ close }) => <PickerForm rooms={rooms} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
) )
} }